]> git.deb.at Git - rhonda/impostor.hazel.git/commitdiff
Implement server-side DTLS handshake
authorMatthew Endsley <mendsley@gmail.com>
Tue, 12 Jan 2021 15:10:12 +0000 (07:10 -0800)
committerMatthew Endsley <mendsley@gmail.com>
Tue, 2 Feb 2021 17:30:32 +0000 (09:30 -0800)
Hazel/Crypto/SpanCryptoExtensions.cs [new file with mode: 0644]
Hazel/Dtls/DtlsConnectionListener.cs
Hazel/Dtls/Handshake.cs
Hazel/Dtls/IRecordProtection.cs
Hazel/Dtls/X25519EcdheRsaSha256.cs
Hazel/Hazel.csproj

diff --git a/Hazel/Crypto/SpanCryptoExtensions.cs b/Hazel/Crypto/SpanCryptoExtensions.cs
new file mode 100644 (file)
index 0000000..b734961
--- /dev/null
@@ -0,0 +1,33 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Hazel.Crypto
+{
+    public static class SpanCryptoExtensions
+    {
+        /// <summary>
+        /// Clear a span's contents to zero
+        /// </summary>
+        public static void SecureClear(this ByteSpan span)
+        {
+            Array.Clear(span.GetUnderlyingArray(), span.Offset, span.Length);
+        }
+
+        /// <summary>
+        /// Fill a byte span with random data
+        /// </summary>
+        /// <param name="random">Entropy source</param>
+        public static void FillWithRandom(this ByteSpan span, RandomNumberGenerator random)
+        {
+            if (span.Offset == 0 && span.Length == span.GetUnderlyingArray().Length)
+            {
+                random.GetBytes(span.GetUnderlyingArray());
+                return;
+            }
+
+            byte[] temp = new byte[span.Length];
+            random.GetBytes(temp);
+            new ByteSpan(temp).CopyTo(span);
+        }
+    }
+}
index dddc9ca7f00f38fc865b8104e82e247c55a6259c..72eabc8f2a2143331e1a2f7a2004f38a25212c53 100644 (file)
@@ -1,5 +1,14 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
 using System.Net;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
 using Hazel.Udp.FewerThreads;
+using Hazel.Crypto;
 
 namespace Hazel.Dtls
 {
@@ -9,6 +18,139 @@ namespace Hazel.Dtls
     /// <inheritdoc />
     public class DtlsConnectionListener : ThreadLimitedUdpConnectionListener
     {
+        const int MaxDatagramSize = 1200;
+
+        /// <summary>
+        /// Current state of handshake sequence
+        /// </summary>
+        enum HandshakeState
+        {
+            ExpectingHello,
+            ExpectingClientKeyExchange,
+            ExpectingChangeCipherSpec,
+            ExpectingFinish,
+        }
+
+        /// <summary>
+        /// State to manage the current epoch `N`
+        /// </summary>
+        struct CurrentEpoch
+        {
+            public ulong NextOutgoingSequence;
+
+            public ulong NextExpectedSequence;
+            public ulong PreviousSequenceWindowBitmask;
+
+            public IRecordProtection RecordProtection;
+
+            // Need to keep these around so we can re-transmit our
+            // last handshake record flight
+            public ByteSpan ExpectedClientFinishedVerification;
+            public ByteSpan ServerFinishedVerification;
+            public ulong NextOutgoingSequenceForPreviousEpoch;
+        }
+
+        /// <summary>
+        /// State to manage the transition from the current
+        /// epoch `N` to epoch `N+1`
+        /// </summary>
+        struct NextEpoch
+        {
+            public ushort Epoch;
+
+            public HandshakeState State;
+            public CipherSuite SelectedCipherSuite;
+
+            public ulong NextOutgoingSequence;
+
+            public IHandshakeCipherSuite Handshake;
+            public IRecordProtection RecordProtection;
+
+            public ByteSpan ClientRandom;
+            public ByteSpan ServerRandom;
+
+            public MemoryStream VerificationStream;
+
+            public ByteSpan ClientVerification;
+            public ByteSpan ServerVerification;
+
+        }
+
+        /// <summary>
+        /// Per-peer state
+        /// </summary>
+        sealed class PeerData : IDisposable
+        {
+            public ushort Epoch;
+            public bool CanHandleApplicationData;
+
+            public CurrentEpoch CurrentEpoch;
+            public NextEpoch  NextEpoch;
+
+            public ConnectionId ConnectionId;
+
+            public readonly List<ByteSpan> QueuedApplicationDataMessage = new List<ByteSpan>();
+
+            public PeerData()
+            {
+                ByteSpan block = new byte[2 * Finished.Size];
+                this.CurrentEpoch.ServerFinishedVerification = block.Slice(0, Finished.Size);
+                this.CurrentEpoch.ExpectedClientFinishedVerification = block.Slice(Finished.Size, Finished.Size);
+
+                ResetPeer(ConnectionId.Create(0), 1);
+            }
+
+            public void ResetPeer(ConnectionId connectionId, ulong nextExpectedSequenceNumber)
+            {
+                Dispose();
+
+                this.Epoch = 0;
+                this.CanHandleApplicationData = false;
+                this.QueuedApplicationDataMessage.Clear();
+
+                this.CurrentEpoch = new CurrentEpoch();
+                this.CurrentEpoch.NextOutgoingSequence = 2; // Account for our ClientHelloVerify
+                this.CurrentEpoch.NextExpectedSequence = nextExpectedSequenceNumber;
+                this.CurrentEpoch.PreviousSequenceWindowBitmask = 0;
+                this.CurrentEpoch.RecordProtection = NullRecordProtection.Instance;
+                this.CurrentEpoch.ServerFinishedVerification.SecureClear();
+                this.CurrentEpoch.ExpectedClientFinishedVerification.SecureClear();
+
+                this.NextEpoch = new NextEpoch();
+                this.NextEpoch.State = HandshakeState.ExpectingHello;
+                this.NextEpoch.RecordProtection = null;
+                this.NextEpoch.ClientRandom = new byte[Random.Size];
+                this.NextEpoch.ServerRandom = new byte[Random.Size];
+                this.NextEpoch.VerificationStream = new MemoryStream();
+                this.NextEpoch.ClientVerification = new byte[Finished.Size];
+                this.NextEpoch.ServerVerification = new byte[Finished.Size];
+
+                this.ConnectionId = connectionId;
+            }
+
+            public void Dispose()
+            {
+                this.CurrentEpoch.RecordProtection?.Dispose();
+                this.NextEpoch.RecordProtection?.Dispose();
+                this.NextEpoch.Handshake?.Dispose();
+                this.NextEpoch.VerificationStream?.Dispose();
+            }
+        }
+
+        private RandomNumberGenerator random;
+
+        // Private key component of certificate's public key
+        private readonly List<ByteSpan> encodedCertificates = new List<ByteSpan>();
+        private uint encodedCertificatesTotalSize;
+        private RSA certificatePrivateKey;
+
+        // HMAC key to validate ClientHello cookie
+        private HMAC currentCookieHmac;
+
+        private readonly ConcurrentDictionary<EndPoint, PeerData> existingPeers = new ConcurrentDictionary<EndPoint, PeerData>();
+
+        private long connectionId_unsafe =  0;
+
         /// <summary>
         /// Create a new instance of the DTLS listener
         /// </summary>
@@ -19,18 +161,1101 @@ namespace Hazel.Dtls
         public DtlsConnectionListener(int numWorkers, IPEndPoint endPoint, ILogger logger, IPMode ipMode = IPMode.IPv4)
             : base(numWorkers, endPoint, logger, ipMode)
         {
+            this.random = RandomNumberGenerator.Create();
+
+            ///TODO(mendsley): The HMAC key should be cycled periodically
+            const string HMACProvider = "System.Security.Cryptography.HMACSHA1";
+            this.currentCookieHmac = HMAC.Create(HMACProvider);
         }
 
         /// <inheritdoc />
-        protected override void ProcessIncomingMessageFromOtherThread(MessageReader message, EndPoint peerAddress, ConnectionId connectionId)
+        protected override void Dispose(bool disposing)
         {
-            base.ProcessIncomingMessageFromOtherThread(message, peerAddress, connectionId);
+            base.Dispose(disposing);
+
+            this.random?.Dispose();
+            this.random = null;
+
+            this.currentCookieHmac?.Dispose();
+            this.currentCookieHmac = null;
+
+            foreach (var pair in this.existingPeers)
+            {
+                pair.Value.Dispose();
+            }
+            this.existingPeers.Clear();
         }
 
-        /// <inheritdoc />
+        /// <summary>
+        /// Set the certificate key pair for the listener
+        /// </summary>
+        /// <param name="certificate">Certificate for the server</param>
+        public void SetCertificate(X509Certificate2 certificate)
+        {
+            if (!certificate.HasPrivateKey)
+            {
+                throw new ArgumentException("Certificate must have a private key attached", nameof(certificate));
+            }
+
+            RSA privateKey = certificate.PrivateKey as RSA;
+            if (privateKey == null)
+            {
+                throw new ArgumentException("Certificate must be signed by an RSA key", nameof(certificate));
+            }
+
+            this.certificatePrivateKey?.Dispose();
+            this.certificatePrivateKey = privateKey;
+
+            // Pre-fragment the certificate data
+            ByteSpan certificateData = Certificate.Encode(certificate);
+            this.encodedCertificatesTotalSize = (uint)certificateData.Length;
+
+            // The first certificate data needs to leave room for
+            //  * Record header
+            //  * ServerHello header
+            //  * ServerHello payload
+            //  * Certificate header
+            int padding = Record.Size + Handshake.Size + ServerHello.Size + Handshake.Size;
+            this.encodedCertificates.Add(certificateData.Slice(0, Math.Min(certificateData.Length, MaxDatagramSize - padding)));
+            certificateData = certificateData.Slice(Math.Min(certificateData.Length, MaxDatagramSize - padding));
+
+            // Subsequent certificate data needs to leave room for
+            //  * Record header
+            //  * Certificate header
+            padding = Record.Size + Handshake.Size;
+            while (certificateData.Length > 0)
+            {
+                this.encodedCertificates.Add(certificateData.Slice(0, Math.Min(certificateData.Length, MaxDatagramSize - padding)));
+                certificateData = certificateData.Slice(Math.Min(certificateData.Length, MaxDatagramSize - padding));
+            }
+        }
+
+        /// <summary>
+        /// Handle an incoming datagram from the network.
+        ///
+        /// This is primarily a wrapper around ProcessIncomingMessage
+        /// to ensure `reader.Recycle()` is always called
+        /// </summary>
+        protected override void ProcessIncomingMessageFromOtherThread(MessageReader reader, EndPoint peerAddress, ConnectionId connectionId)
+        {
+            ByteSpan message = new ByteSpan(reader.Buffer, reader.Offset + reader.Position, reader.BytesRemaining);
+            this.ProcessIncomingMessage(message, peerAddress);
+            reader.Recycle();
+        }
+
+        /// <summary>
+        /// Handle an incoming datagram from the network
+        /// </summary>
+        private void ProcessIncomingMessage(ByteSpan message, EndPoint peerAddress)
+        {
+            PeerData peer = null;
+            if (!this.existingPeers.TryGetValue(peerAddress, out peer))
+            {
+                HandleNonPeerRecord(message, peerAddress);
+                return;
+            }
+
+            lock (peer)
+            {
+                // Each incoming packet may contain multiple DTLS
+                // records
+                while (message.Length > 0)
+                {
+                    Record record;
+                    if (!Record.Parse(out record, message))
+                    {
+                        this.Logger.WriteError($"Dropping malformed record from `{peerAddress}`");
+                        return;
+                    }
+                    message = message.Slice(Record.Size);
+
+                    if (message.Length < record.Length)
+                    {
+                        this.Logger.WriteError($"Dropping malformed record from `{peerAddress}` Length({record.Length}) AvailableBytes({message.Length})");
+                        return;
+                    }
+
+                    ByteSpan recordPayload = message.Slice(0, record.Length);
+                    message = message.Slice(record.Length);
+
+                    // Early-out and drop ApplicationData records
+                    if (record.ContentType == ContentType.ApplicationData && !peer.CanHandleApplicationData)
+                    {
+                        this.Logger.WriteInfo($"Dropping ApplicationData record from `{peerAddress}` Cannot process yet");
+                        continue;
+                    }
+
+                    // Drop records from a different epoch
+                    if (record.Epoch != peer.Epoch)
+                    {
+                        // Handle existing client negotiating a new connection
+                        if (record.Epoch == 0 && record.ContentType == ContentType.Handshake)
+                        {
+                            ByteSpan handshakePayload = recordPayload;
+
+                            Handshake handshake;
+                            if (!Handshake.Parse(out handshake, recordPayload))
+                            {
+                                this.Logger.WriteError($"Dropping malformed re-negotiation Handshake from `{peerAddress}`");
+                                continue;
+                            }
+                            handshakePayload = handshakePayload.Slice(Handshake.Size);
+
+                            if (handshake.FragmentOffset != 0 || handshake.Length != handshake.FragmentLength)
+                            {
+                                this.Logger.WriteError($"Dropping fragmented re-negotiation Handshake from `{peerAddress}`");
+                                continue;
+                            }
+                            else if (handshake.MessageType != HandshakeType.ClientHello)
+                            {
+                                this.Logger.WriteError($"Dropping non-ClientHello re-negotiation Handshake from `{peerAddress}`");
+                                continue;
+                            }
+                            else if (handshakePayload.Length < handshake.Length)
+                            {
+                                this.Logger.WriteError($"Dropping malformed re-negotiation Handshake from `{peerAddress}`: Length({handshake.Length}) AvailableBytes({handshakePayload.Length})");
+                            }
+
+                            if (!this.HandleClientHello(peer, peerAddress, ref record, ref handshake, recordPayload, handshakePayload))
+                            {
+                                return;
+                            }
+                            continue;
+                        }
+
+                        this.Logger.WriteError($"Dropping bad-epoch record from `{peerAddress}` RecordEpoch({record.Epoch}) CurrentEpoch({peer.Epoch})");
+                        continue;
+                    }
+
+                    // Prevent replay attacks by dropping records
+                    // we've already processed
+                    int windowIndex = (int)(peer.CurrentEpoch.NextExpectedSequence - record.SequenceNumber - 1);
+                    ulong windowMask = 1ul << windowIndex;
+                    if (record.SequenceNumber < peer.CurrentEpoch.NextExpectedSequence)
+                    {
+                        if (windowIndex >= 64)
+                        {
+                            this.Logger.WriteInfo($"Dropping too-old record from `{peerAddress}` Sequence({record.SequenceNumber}) Expected({peer.CurrentEpoch.NextExpectedSequence})");
+                            continue;
+                        }
+
+                        if ((peer.CurrentEpoch.PreviousSequenceWindowBitmask & windowMask) != 0)
+                        {
+                            this.Logger.WriteInfo($"Dropping duplicate record from `{peerAddress}`");
+                            continue;
+                        }
+                    }
+
+                    // Validate record authenticity
+                    int decryptedSize = peer.CurrentEpoch.RecordProtection.GetDecryptedSize(recordPayload.Length);
+                    ByteSpan decryptedPayload = ReuseSpanIfPossible(recordPayload, decryptedSize);
+
+                    if (!peer.CurrentEpoch.RecordProtection.DecryptCiphertextFromClient(decryptedPayload, recordPayload, ref record))
+                    {
+                        this.Logger.WriteError($"Dropping non-authentic record from `{peerAddress}`");
+                        return;
+                    }
+
+                    recordPayload = decryptedPayload;
+
+                    // Update our squence number bookeeping
+                    if (record.SequenceNumber >= peer.CurrentEpoch.NextExpectedSequence)
+                    {
+                        int windowShift = (int)(record.SequenceNumber + 1 - peer.CurrentEpoch.NextExpectedSequence);
+                        peer.CurrentEpoch.PreviousSequenceWindowBitmask <<= windowShift;
+                        peer.CurrentEpoch.NextExpectedSequence = record.SequenceNumber + 1;
+                    }
+                    else
+                    {
+                        peer.CurrentEpoch.PreviousSequenceWindowBitmask |= windowMask;
+                    }
+
+                    switch (record.ContentType)
+                    {
+                        case ContentType.ChangeCipherSpec:
+                            if (peer.NextEpoch.State != HandshakeState.ExpectingChangeCipherSpec)
+                            {
+                                this.Logger.WriteError($"Dropping unexpected ChangeChiperSpec record from `{peerAddress}` State({peer.NextEpoch.State})");
+                                break;
+                            }
+                            else if (peer.NextEpoch.RecordProtection == null)
+                            {
+                                ///NOTE(mendsley): This _should_ not
+                                /// happen on a well-formed server.
+                                Debug.Assert(false, "How did we receive a ChangeCipherSpec message without a pending record protection instance?");
+
+                                this.Logger.WriteError($"Dropping ChangeCipherSpec message from `{peerAddress}`: No pending record protection");
+                                break;
+                            }
+
+                            // Migrate to the next epoch
+                            peer.Epoch = peer.NextEpoch.Epoch;
+                            peer.CanHandleApplicationData = false; // Need a Finished message
+                            peer.CurrentEpoch.NextOutgoingSequenceForPreviousEpoch = peer.CurrentEpoch.NextOutgoingSequence;
+                            peer.CurrentEpoch.RecordProtection = peer.NextEpoch.RecordProtection;
+                            peer.CurrentEpoch.NextOutgoingSequence = 1;
+                            peer.CurrentEpoch.NextExpectedSequence = 1;
+                            peer.CurrentEpoch.PreviousSequenceWindowBitmask = 0;
+                            peer.NextEpoch.ClientVerification.CopyTo(peer.CurrentEpoch.ExpectedClientFinishedVerification);
+                            peer.NextEpoch.ServerVerification.CopyTo(peer.CurrentEpoch.ServerFinishedVerification);
+
+                            peer.NextEpoch.State = HandshakeState.ExpectingHello;
+                            peer.NextEpoch.Handshake?.Dispose();
+                            peer.NextEpoch.Handshake = null;
+                            peer.NextEpoch.NextOutgoingSequence = 1;
+                            peer.NextEpoch.RecordProtection = null;
+                            peer.NextEpoch.VerificationStream.SetLength(0);
+                            peer.NextEpoch.ClientVerification.SecureClear();
+                            peer.NextEpoch.ServerVerification.SecureClear();
+                            break;
+
+                        case ContentType.Alert:
+                            this.Logger.WriteError($"Dropping unsupported Alert record from `{peerAddress}`");
+                            break;
+
+                        case ContentType.Handshake:
+                            if (!ProcessHandshake(peer, peerAddress, ref record, recordPayload))
+                            {
+                                return;
+                            }
+                            break;
+
+                        case ContentType.ApplicationData:
+                            // Forward data to the application
+                            MessageReader reader = MessageReader.GetSized(recordPayload.Length);
+                            reader.Length = recordPayload.Length;
+                            recordPayload.CopyTo(reader.Buffer);
+
+                            base.ProcessIncomingMessageFromOtherThread(reader, peerAddress, peer.ConnectionId);
+                            break;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Reuse an existing span if there is enough space,
+        /// otherwise allocate new storage
+        /// </summary>
+        /// <param name="source">
+        /// Source span we should attempt to reuse
+        /// </param>
+        /// <param name="requiredSize">Required size (bytes)</param>
+        private static ByteSpan ReuseSpanIfPossible(ByteSpan source, int requiredSize)
+        {
+            if (source.Length >= requiredSize)
+            {
+                return source.Slice(0, requiredSize);
+            }
+
+            return new byte[requiredSize];
+        }
+
+        /// <summary>
+        /// Process an incoming Handshake protocol message
+        /// </summary>
+        /// <param name="peer">Originating peer</param>
+        /// <param name="peerAddress">Peer's network address</param>
+        /// <param name="record">Parent record</param>
+        /// <param name="message">Record payload</param>
+        /// <returns>
+        /// True if further processing of the underlying datagram
+        /// should be continues. Otherwise, false.
+        /// </returns>
+        private bool ProcessHandshake(PeerData peer, EndPoint peerAddress, ref Record record, ByteSpan message)
+        {
+            // Each record may have multiple handshake payloads
+            while (message.Length > 0)
+            {
+                ByteSpan originalMessage = message;
+
+                Handshake handshake;
+                if (!Handshake.Parse(out handshake, message))
+                {
+                    this.Logger.WriteError($"Dropping malformed Handshake message from `{peerAddress}`");
+                    return false;
+                }
+                message = message.Slice(Handshake.Size);
+
+                if (message.Length < handshake.Length)
+                {
+                    this.Logger.WriteError($"Dropping malformed Handshake message from `{peerAddress}`");
+                    return false;
+                }
+
+                ByteSpan payload = message.Slice(0, (int)message.Length);
+                message = message.Slice((int)handshake.Length);
+                originalMessage = originalMessage.Slice(0, Handshake.Size + (int)handshake.Length);
+
+                // We do not support fragmented handshake messages
+                // from the client
+                if (handshake.FragmentOffset != 0 || handshake.FragmentLength != handshake.Length)
+                {
+                    this.Logger.WriteError($"Dropping fragmented Handshake message from `{peerAddress}` Offset({handshake.FragmentOffset}) FragmentLength({handshake.FragmentLength}) Length({handshake.Length})");
+                    continue;
+                }
+
+                ByteSpan packet;
+                ByteSpan writer;
+
+                switch (handshake.MessageType)
+                {
+                    case HandshakeType.ClientHello:
+                        if (!this.HandleClientHello(peer, peerAddress, ref record, ref handshake, originalMessage, payload))
+                        {
+                            return false;
+                        }
+                        break;
+
+                    case HandshakeType.ClientKeyExchange:
+                        if (peer.NextEpoch.State != HandshakeState.ExpectingClientKeyExchange)
+                        {
+                            this.Logger.WriteError($"Dropping unexpected ClientKeyExchange message form `{peerAddress}` State({peer.NextEpoch.State})");
+                            continue;
+                        }
+                        else if (handshake.MessageSequence != 5)
+                        {
+                            this.Logger.WriteError($"Dropping bad-sequence ClientKeyExchange message from `{peerAddress}` MessageSequence({handshake.MessageSequence})");
+                            continue;
+                        }
+
+                        ByteSpan sharedSecret = new byte[peer.NextEpoch.Handshake.SharedKeySize()];
+                        if (!peer.NextEpoch.Handshake.VerifyClientMessageAndGenerateSharedKey(sharedSecret, payload))
+                        {
+                            this.Logger.WriteError($"Dropping malformed ClientKeyExchange message from `{peerAddress}`");
+                            return false;
+                        }
+
+                        // Record incoming ClientKeyExchange message
+                        // to verification stream
+                        peer.NextEpoch.VerificationStream.Write(
+                            originalMessage.GetUnderlyingArray()
+                            , originalMessage.Offset
+                            , originalMessage.Length
+                        );
+
+                        ByteSpan randomSeed = new byte[2 * Random.Size];
+                        peer.NextEpoch.ClientRandom.CopyTo(randomSeed);
+                        peer.NextEpoch.ServerRandom.CopyTo(randomSeed.Slice(Random.Size));
+
+                        const int MasterSecretSize = 48;
+                        ByteSpan masterSecret = new byte[MasterSecretSize];
+                        PrfSha256.ExpandSecret(
+                            masterSecret
+                            , sharedSecret
+                            , PrfLabel.MASTER_SECRET
+                            , randomSeed
+                        );
+
+                        // Create the record protection for the upcoming epoch
+                        switch (peer.NextEpoch.SelectedCipherSuite)
+                        {
+                            case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+                                peer.NextEpoch.RecordProtection = new Aes128GcmRecordProtection(
+                                    masterSecret
+                                    , peer.NextEpoch.ServerRandom
+                                    , peer.NextEpoch.ClientRandom);
+                                break;
+
+                            default:
+                                Debug.Assert(false, $"How did we agree to a cipher suite {peer.NextEpoch.SelectedCipherSuite} we can't create?");
+                                this.Logger.WriteError($"Dropping ClientKeyExchange message from `{peerAddress}` Unsuppored cipher suite");
+                                return false;
+                        }
+
+                        // Generate verification signatures
+                        ByteSpan handshakeStreamHash;
+                        using (SHA256 sha256 = SHA256.Create())
+                        {
+                            peer.NextEpoch.VerificationStream.Position = 0;
+                            handshakeStreamHash = sha256.ComputeHash(peer.NextEpoch.VerificationStream);
+                        }
+
+                        PrfSha256.ExpandSecret(
+                            peer.NextEpoch.ClientVerification
+                            , masterSecret
+                            , PrfLabel.CLIENT_FINISHED
+                            , handshakeStreamHash
+                        );
+                        PrfSha256.ExpandSecret(
+                            peer.NextEpoch.ServerVerification
+                            , masterSecret
+                            , PrfLabel.SERVER_FINISHED
+                            , handshakeStreamHash
+                        );
+
+
+                        // Update handshake state
+                        masterSecret.SecureClear();
+                        peer.NextEpoch.State = HandshakeState.ExpectingChangeCipherSpec;
+                        break;
+
+                    case HandshakeType.Finished:
+                        // Unlike other handshake messages, this is
+                        // for the current epoch - not the next epoch
+
+                        // Cannot process a Finished message for
+                        // epoch 0
+                        if (peer.Epoch == 0)
+                        {
+                            this.Logger.WriteError($"Dropping Finished message for 0-epoch from `{peerAddress}`");
+                            continue;
+                        }
+                        // Cannot process a Finished message when we
+                        // are negotiating the next epoch
+                        else if (peer.NextEpoch.State != HandshakeState.ExpectingHello)
+                        {
+                            this.Logger.WriteError($"Dropping Finished message while negotiating new epoch from `{peerAddress}`");
+                            continue;
+                        }
+                        // Cannot process a Finished message without
+                        // verify data
+                        else if (peer.CurrentEpoch.ExpectedClientFinishedVerification.Length != Finished.Size || peer.CurrentEpoch.ServerFinishedVerification.Length != Finished.Size)
+                        {
+                            ///NOTE(mendsley): This _should_ not
+                            /// happen on a well-formed server.
+                            Debug.Assert(false, "How do we have an established non-zero epoch without verify data");
+
+                            this.Logger.WriteError($"Dropping Finished message (no verify data) from `{peerAddress}`");
+                            return false;
+                        }
+
+                        // Verify message sequence
+                        if (handshake.MessageSequence != 6)
+                        {
+                            this.Logger.WriteError($"Dropping bad-sequence Finished message from `{peerAddress}` MessageSequence({handshake.MessageSequence})");
+                            continue;
+                        }
+
+                        // Verify the client has the correct
+                        // handshake sequence
+                        if (payload.Length != Finished.Size)
+                        {
+                            this.Logger.WriteError($"Dropping malformed Finished message from `{peerAddress}`");
+                            return false;
+                        }
+                        else if (1 != Crypto.Const.ConstantCompareSpans(payload, peer.CurrentEpoch.ExpectedClientFinishedVerification))
+                        {
+                            this.Logger.WriteError($"Dropping non-verified Finished Handshake from `{peerAddress}`");
+
+                            // Abort the connection here
+                            //
+                            // The client is either broken, or
+                            // doen not agree on our epoch settings.
+                            //
+                            // Either way, there is not a feasible
+                            // way to progress the connection.
+                            base.MarkConnectionAsStale(peer.ConnectionId);
+                            this.existingPeers.TryRemove(peerAddress, out peer);
+                            return false;
+                        }
+
+                        // Describe our ChangeCipherSpec+Finished
+                        Handshake outgoingHandshake = new Handshake();
+                        outgoingHandshake.MessageType = HandshakeType.Finished;
+                        outgoingHandshake.Length = Finished.Size;
+                        outgoingHandshake.MessageSequence = 7;
+                        outgoingHandshake.FragmentOffset = 0;
+                        outgoingHandshake.FragmentLength = outgoingHandshake.Length;
+
+                        Record changeCipherSpecRecord = new Record();
+                        changeCipherSpecRecord.ContentType = ContentType.ChangeCipherSpec;
+                        changeCipherSpecRecord.Epoch = (ushort)(peer.Epoch - 1);
+                        changeCipherSpecRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequenceForPreviousEpoch;
+                        changeCipherSpecRecord.Length = 0;
+                        ++peer.CurrentEpoch.NextOutgoingSequenceForPreviousEpoch;
+
+                        int plaintextFinishedPayloadSize = Handshake.Size + (int)outgoingHandshake.Length;
+                        Record finishedRecord = new Record();
+                        finishedRecord.ContentType = ContentType.Handshake;
+                        finishedRecord.Epoch = peer.Epoch;
+                        finishedRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequence;
+                        finishedRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(plaintextFinishedPayloadSize);
+                        ++peer.CurrentEpoch.NextOutgoingSequence;
+
+                        // Encode the flight into wire format
+                        packet = new byte[Record.Size + Record.Size + finishedRecord.Length];
+                        writer = packet;
+                        changeCipherSpecRecord.Encode(writer);
+                        writer = writer.Slice(Record.Size);
+                        finishedRecord.Encode(writer);
+                        writer = writer.Slice(Record.Size);
+                        outgoingHandshake.Encode(writer);
+                        writer = writer.Slice(Handshake.Size);
+                        peer.CurrentEpoch.ServerFinishedVerification.CopyTo(writer);
+
+                        // Protect the Finished Handshake record
+                        peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                            packet.Slice(Record.Size + Record.Size, finishedRecord.Length)
+                            , packet.Slice(Record.Size + Record.Size, plaintextFinishedPayloadSize)
+                            , ref finishedRecord
+                        );
+
+                        // Current epoch can now handle application data
+                        peer.CanHandleApplicationData = true;
+
+                        base.QueueRawData(packet, peerAddress);
+                        break;
+
+                    // Drop messages that we do not support
+                    case HandshakeType.CertificateVerify:
+                        this.Logger.WriteError($"Dropping unsupported Handshake message from `{peerAddress}` MessageType({handshake.MessageType})");
+                        continue;
+
+                    // Drop messages that originate from the server
+                    case HandshakeType.HelloRequest:
+                    case HandshakeType.ServerHello:
+                    case HandshakeType.HelloVerifyRequest:
+                    case HandshakeType.Certificate:
+                    case HandshakeType.ServerKeyExchange:
+                    case HandshakeType.CertificateRequest:
+                    case HandshakeType.ServerHelloDone:
+                        this.Logger.WriteError($"Dropping server Handshake message from `{peerAddress}` MessageType({handshake.MessageType})");
+                        continue;
+                }
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Handle a ClientHello message for a peer
+        /// </summary>
+        /// <param name="peer">Originating peer</param>
+        /// <param name="peerAddress">Peer address</param>
+        /// <param name="record">Parent record</param>
+        /// <param name="handshake">Parent Handshake header</param>
+        /// <param name="payload">Handshake payload</param>
+        private bool HandleClientHello(PeerData peer, EndPoint peerAddress, ref Record record, ref Handshake handshake, ByteSpan originalMessage, ByteSpan payload)
+        {
+            // Verify message sequence
+            if (handshake.MessageSequence != 0)
+            {
+                this.Logger.WriteError($"Dropping bad-sequence ClientHello from `{peerAddress}` MessageSequence({handshake.MessageSequence})`");
+                return true;
+            }
+
+            // Make sure we can handle a ClientHello message
+            if (peer.NextEpoch.State != HandshakeState.ExpectingHello && peer.NextEpoch.State != HandshakeState.ExpectingClientKeyExchange)
+            {
+                // Always handle ClientHello for epoch 0
+                if (record.Epoch != 0)
+                {
+                    this.Logger.WriteError($"Dropping ClientHello from `{peer}` Not expecting ClientHello");
+                    return true;
+                }
+            }
+
+            ClientHello clientHello;
+            if (!ClientHello.Parse(out clientHello, payload))
+            {
+                this.Logger.WriteError($"Dropping malformed ClientHello Handshake message from `{peerAddress}`");
+                return false;
+            }
+
+            // Find an acceptable cipher suite we can use
+            CipherSuite selectedCipherSuite = CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256;
+            if (!clientHello.ContainsCipherSuite(CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) || !clientHello.ContainsCurve(NamedCurve.x25519))
+            {
+                this.Logger.WriteError($"Dropping ClientHello from `{peerAddress}` No compatible cipher suite");
+                return false;
+            }
+
+            // If this message was not signed by us,
+            // request a signed message before doing anything else
+            if (!HelloVerifyRequest.VerifyCookie(clientHello.Cookie, peerAddress, this.currentCookieHmac))
+            {
+                ulong outgoingSequence = 1;
+                IRecordProtection recordProtection = NullRecordProtection.Instance;
+                if (record.Epoch != 0)
+                {
+                    outgoingSequence = peer.CurrentEpoch.NextExpectedSequence;
+                    ++peer.CurrentEpoch.NextOutgoingSequenceForPreviousEpoch;
+
+                    recordProtection = peer.CurrentEpoch.RecordProtection;
+                }
+
+                this.SendHelloVerifyRequest(peerAddress, outgoingSequence, record.Epoch, recordProtection);
+                return true;
+            }
+
+            // Client is initiating a brand new connection. We need
+            // to destroy the existing connection and establish a
+            // new session.
+            if (record.Epoch == 0 && peer.Epoch != 0)
+            {
+                ConnectionId oldConnectionId = peer.ConnectionId;
+                peer.ResetPeer(this.AllocateConnectionId(), record.SequenceNumber + 1);
+
+                // Inform the parent layer that the existing
+                // connection should be abandoned.
+                base.MarkConnectionAsStale(oldConnectionId);
+            }
+
+            // Determine if this is an original message, or a retransmission
+            bool recordMessagesForVerifyData = false;
+            if (peer.NextEpoch.State == HandshakeState.ExpectingHello)
+            {
+                // Create our handhake cipher suite
+                IHandshakeCipherSuite handshakeCipherSuite = null;
+                switch (selectedCipherSuite)
+                {
+                    case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+                        if (clientHello.ContainsCurve(NamedCurve.x25519))
+                        {
+                            handshakeCipherSuite = new X25519EcdheRsaSha256(this.random);
+                        }
+                        else
+                        {
+                            this.Logger.WriteError($"Dropping ClientHello from `{peerAddress}` Could not create TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 cipher suite");
+                            return false;
+                        }
+
+                        break;
+
+                    default:
+                        this.Logger.WriteError($"Dropping ClientHello from `{peerAddress}` Could not create handshake cipher suite");
+                        return false;
+                }
+
+                // Update the state of our epoch transition
+                peer.NextEpoch.Epoch = (ushort)(record.Epoch + 1);
+                peer.NextEpoch.State = HandshakeState.ExpectingClientKeyExchange;
+                peer.NextEpoch.SelectedCipherSuite = selectedCipherSuite;
+                peer.NextEpoch.Handshake = handshakeCipherSuite;
+                clientHello.Random.CopyTo(peer.NextEpoch.ClientRandom);
+                peer.NextEpoch.ServerRandom.FillWithRandom(this.random);
+                recordMessagesForVerifyData = true;
+
+                // Copy the original ClientHello
+                // handshake to our verification stream
+                peer.NextEpoch.VerificationStream.Write(
+                    originalMessage.GetUnderlyingArray()
+                    , originalMessage.Offset
+                    , Handshake.Size + (int)handshake.Length
+                );
+            }
+
+            // The initial record flight from the server
+            // contains the following Handshake messages:
+            //    * ServerHello
+            //    * Certificate
+            //    * ServerKeyExchange
+            //    * ServerHelloDone
+            //
+            // The Certificate message is almost always
+            // too large to fit into a single datagram,
+            // so it is pre-fragmented
+            // (see `SetCertificates`). Therefore, we
+            // need to send multiple record packets for
+            // this flight.
+            //
+            // The first record contains the ServerHello
+            // handshake message, as well as the first
+            // portion of the Certificate message.
+            //
+            // We then send a record packet until the
+            // entire Certificate message has been sent
+            // to the client.
+            //
+            // The final record packet contains the
+            // ServerKeyExchange and the ServerHelloDone
+            // messages.
+
+            // Describe first record of the flight
+            ServerHello serverHello = new ServerHello();
+            serverHello.Random = peer.NextEpoch.ServerRandom;
+            serverHello.CipherSuite = selectedCipherSuite;
+
+            Handshake serverHelloHandshake = new Handshake();
+            serverHelloHandshake.MessageType = HandshakeType.ServerHello;
+            serverHelloHandshake.Length = ServerHello.Size;
+            serverHelloHandshake.MessageSequence = 1;
+            serverHelloHandshake.FragmentOffset = 0;
+            serverHelloHandshake.FragmentLength = serverHelloHandshake.Length;
+
+            Handshake certificateHandshake = new Handshake();
+            certificateHandshake.MessageType = HandshakeType.Certificate;
+            certificateHandshake.Length = this.encodedCertificatesTotalSize;
+            certificateHandshake.MessageSequence = 2;
+            certificateHandshake.FragmentOffset = 0;
+            certificateHandshake.FragmentLength = (uint)this.encodedCertificates[0].Length;
+
+            int initialRecordPayloadSize = 0
+                + Handshake.Size + ServerHello.Size
+                + Handshake.Size + (int)certificateHandshake.FragmentLength
+                ;
+            Record initialRecord = new Record();
+            initialRecord.ContentType = ContentType.Handshake;
+            initialRecord.Epoch = peer.Epoch;
+            initialRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequence;
+            initialRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(initialRecordPayloadSize);
+            ++peer.CurrentEpoch.NextOutgoingSequence;
+
+            // Convert initial record of the flight to
+            // wire format
+            ByteSpan packet = new byte[Record.Size + initialRecord.Length];
+            ByteSpan writer = packet;
+            initialRecord.Encode(writer);
+            writer = writer.Slice(Record.Size);
+            serverHelloHandshake.Encode(writer);
+            writer = writer.Slice(Handshake.Size);
+            serverHello.Encode(writer);
+            writer = writer.Slice(ServerHello.Size);
+            certificateHandshake.Encode(writer);
+            writer = writer.Slice(Handshake.Size);
+            this.encodedCertificates[0].CopyTo(writer);
+
+            // Protect initial record of the flight
+            peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                packet.Slice(Record.Size, initialRecord.Length)
+                , packet.Slice(Record.Size, initialRecordPayloadSize)
+                , ref initialRecord
+            );
+
+            base.QueueRawData(packet, peerAddress);
+
+            // Record record payload for verification
+            if (recordMessagesForVerifyData)
+            {
+                Handshake fullCeritficateHandshake = certificateHandshake;
+                fullCeritficateHandshake.FragmentLength = fullCeritficateHandshake.Length;
+
+                packet = new byte[Handshake.Size + ServerHello.Size + Handshake.Size];
+                writer = packet;
+                serverHelloHandshake.Encode(writer);
+                writer = writer.Slice(Handshake.Size);
+                serverHello.Encode(writer);
+                writer = writer.Slice(ServerHello.Size);
+                certificateHandshake.Encode(writer);
+                writer = writer.Slice(Handshake.Size);
+
+                peer.NextEpoch.VerificationStream.Write(
+                    packet.GetUnderlyingArray()
+                    , packet.Offset
+                    , packet.Length
+                );
+                foreach (ByteSpan span in this.encodedCertificates)
+                {
+                    peer.NextEpoch.VerificationStream.Write(
+                        span.GetUnderlyingArray()
+                        , span.Offset
+                        , span.Length
+                    );
+                }
+            }
+
+            // Process additional certificate records
+            for (int ii = 1, nn = this.encodedCertificates.Count; ii != nn; ++ii)
+            {
+                certificateHandshake.FragmentOffset += certificateHandshake.FragmentLength;
+                certificateHandshake.FragmentLength = (uint)this.encodedCertificates[ii].Length;
+
+                int additionalRecordPayloadSize = Handshake.Size + (int)certificateHandshake.FragmentLength;
+                Record additionalRecord = new Record();
+                additionalRecord.ContentType = ContentType.Handshake;
+                additionalRecord.Epoch = peer.Epoch;
+                additionalRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequence;
+                additionalRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(additionalRecordPayloadSize);
+                ++peer.CurrentEpoch.NextOutgoingSequence;
+
+                // Convert record to wire format
+                packet = new byte[Record.Size + additionalRecord.Length];
+                writer = packet;
+                additionalRecord.Encode(writer);
+                writer = writer.Slice(Record.Size);
+                certificateHandshake.Encode(writer);
+                writer = writer.Slice(Handshake.Size);
+                this.encodedCertificates[ii].CopyTo(writer);
+
+                // Protect record
+                peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                    packet.Slice(Record.Size, additionalRecord.Length)
+                    , packet.Slice(Record.Size, additionalRecordPayloadSize)
+                    , ref additionalRecord
+                );
+
+                base.QueueRawData(packet, peerAddress);
+            }
+
+            // Describe final record of the flight
+            Handshake serverKeyExchangeHandshake = new Handshake();
+            serverKeyExchangeHandshake.MessageType = HandshakeType.ServerKeyExchange;
+            serverKeyExchangeHandshake.Length = (uint)peer.NextEpoch.Handshake.CalculateServerMessageSize(this.certificatePrivateKey);
+            serverKeyExchangeHandshake.MessageSequence = 3;
+            serverKeyExchangeHandshake.FragmentOffset = 0;
+            serverKeyExchangeHandshake.FragmentLength = serverKeyExchangeHandshake.Length;
+
+            Handshake serverHelloDoneHandshake = new Handshake();
+            serverHelloDoneHandshake.MessageType = HandshakeType.ServerHelloDone;
+            serverHelloDoneHandshake.Length = 0;
+            serverHelloDoneHandshake.MessageSequence = 4;
+            serverHelloDoneHandshake.FragmentOffset = 0;
+            serverHelloDoneHandshake.FragmentLength = 0;
+
+            int finalRecordPayloadSize = 0
+                + Handshake.Size + (int)serverKeyExchangeHandshake.Length
+                + Handshake.Size + (int)serverHelloDoneHandshake.Length
+                ;
+            Record finalRecord = new Record();
+            finalRecord.ContentType = ContentType.Handshake;
+            finalRecord.Epoch = peer.Epoch;
+            finalRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequence;
+            finalRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(finalRecordPayloadSize);
+            ++peer.CurrentEpoch.NextOutgoingSequence;
+
+            // Convert final record of the flight to wire
+            // format
+            packet = new byte[Record.Size + finalRecord.Length];
+            writer = packet;
+            finalRecord.Encode(writer);
+            writer = writer.Slice(Record.Size);
+            serverKeyExchangeHandshake.Encode(writer);
+            writer = writer.Slice(Handshake.Size);
+            peer.NextEpoch.Handshake.EncodeServerKeyExchangeMessage(writer, this.certificatePrivateKey);
+            writer = writer.Slice((int)serverKeyExchangeHandshake.Length);
+            serverHelloDoneHandshake.Encode(writer);
+
+            // Record record payload for verification
+            if (recordMessagesForVerifyData)
+            {
+                peer.NextEpoch.VerificationStream.Write(
+                    packet.GetUnderlyingArray()
+                    , packet.Offset + Record.Size
+                    , finalRecordPayloadSize
+                 );
+            }
+
+            // Protect final record of the flight
+            peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                packet.Slice(Record.Size, finalRecord.Length)
+                , packet.Slice(Record.Size, finalRecordPayloadSize)
+                , ref finalRecord
+            );
+
+            base.QueueRawData(packet, peerAddress);
+
+            return true;
+        }
+
+        /// <summary>
+        /// Handle an incoming packet that is not tied to an existing peer
+        /// </summary>
+        /// <param name="message">Incoming datagram</param>
+        /// <param name="peerAddress">Originating address</param>
+        private void HandleNonPeerRecord(ByteSpan message, EndPoint peerAddress)
+        {
+            Record record;
+            if (!Record.Parse(out record, message))
+            {
+                this.Logger.WriteError($"Dropping malformed record from non-peer `{peerAddress}`");
+                return;
+            }
+            message = message.Slice(Record.Size);
+
+            // The protocol only supports receiving a single record
+            // from a non-peer.
+            if (record.Length != message.Length)
+            {
+                ///NOTE(mendsley): This isn't always fatal.
+                /// However, this is an indication that something
+                /// fishy is going on. In the best case, there's a
+                /// bug on the client or in the UDP stack (some
+                /// stacks don't both to verify the checksum). In the
+                /// worst case we're dealing with a malicious actor.
+                /// In the malicious case, we'll end up dropping the
+                /// connection later in the process.
+                this.Logger.WriteInfo($"Received multiple record from non-peer `{peerAddress}`. Dropping all but first");
+                if (message.Length < record.Length)
+                {
+                    return;
+                }
+            }
+
+            // We only accept zero-epoch records from non-peers
+            if (record.Epoch != 0)
+            {
+                ///NOTE(mendsley): Not logging anything here, as
+                /// this could easily be latent data arriving from a
+                /// recently disconnected peer.
+                return;
+            }
+
+            // We only accept Handshake protocol messages from non-peers
+            if (record.ContentType != ContentType.Handshake)
+            {
+                this.Logger.WriteError($"Dropping non-handhsake message from non-peer `{peerAddress}`");
+                return;
+            }
+
+            ByteSpan originalMessage = message;
+
+            Handshake handshake;
+            if (!Handshake.Parse(out handshake, message))
+            {
+                this.Logger.WriteError($"Dropping malformed handshake message from non-peer `{peerAddress}`");
+                return;
+            }
+
+            // We only accept ClientHello messages from non-peers
+            if (handshake.MessageType != HandshakeType.ClientHello)
+            {
+                this.Logger.WriteError($"Dropping non-ClientHello ({handshake.MessageType}) message from non-peer `{peerAddress}`");
+                return;
+            }
+            message = message.Slice(Handshake.Size);
+
+            ClientHello clientHello;
+            if (!ClientHello.Parse(out clientHello, message))
+            {
+                this.Logger.WriteError($"Dropping malformed ClientHello message from non-peer `{peerAddress}`");
+                return;
+            }
+
+            // If this ClientHello is not signed by us, request the
+            // client send us a signed message
+            if (!HelloVerifyRequest.VerifyCookie(clientHello.Cookie, peerAddress, this.currentCookieHmac))
+            {
+                this.SendHelloVerifyRequest(peerAddress, 1, 0, NullRecordProtection.Instance);
+                return;
+            }
+
+            // Allocate state for the new peer and register it
+            PeerData peer = new PeerData();
+            peer.ResetPeer(this.AllocateConnectionId(), record.SequenceNumber + 1);
+
+            this.existingPeers[peerAddress] = peer;
+
+            lock (peer)
+            {
+                this.ProcessHandshake(peer, peerAddress, ref record, originalMessage);
+            }
+        }
+
+        //Send a HelloVerifyRequest handshake message to a peer
+        private void SendHelloVerifyRequest(EndPoint peerAddress, ulong recordSequence, ushort epoch, IRecordProtection recordProtection)
+        {
+            Handshake handshake = new Handshake();
+            handshake.MessageType = HandshakeType.HelloVerifyRequest;
+            handshake.Length = HelloVerifyRequest.Size;
+            handshake.MessageSequence = 0;
+            handshake.FragmentOffset = 0;
+            handshake.FragmentLength = handshake.Length;
+
+            int plaintextPayloadSize = Handshake.Size + (int)handshake.Length;
+
+            Record record = new Record();
+            record.ContentType = ContentType.Handshake;
+            record.Epoch = epoch;
+            record.SequenceNumber = recordSequence;
+            record.Length = (ushort)recordProtection.GetEncryptedSize(plaintextPayloadSize);
+
+            // Encode record to wire format
+            ByteSpan packet = new byte[Record.Size + record.Length];
+            ByteSpan writer = packet;
+            record.Encode(writer);
+            writer = writer.Slice(Record.Size);
+            handshake.Encode(writer);
+            writer = writer.Slice(Handshake.Size);
+            HelloVerifyRequest.Encode(writer, peerAddress, this.currentCookieHmac);
+
+            // Protect record payload
+            recordProtection.EncryptServerPlaintext(
+                packet.Slice(Record.Size, record.Length)
+                , packet.Slice(Record.Size, plaintextPayloadSize)
+                , ref record
+            );
+
+            base.QueueRawData(packet, peerAddress);
+        }
+
+        /// <summary>
+        /// Handle a requrest to send a datagram to the network
+        /// </summary>
         protected override void QueueRawData(ByteSpan span, EndPoint remoteEndPoint)
         {
-            base.QueueRawData(span, remoteEndPoint);
+            PeerData peer;
+            if (!this.existingPeers.TryGetValue(remoteEndPoint, out peer))
+            {
+                // Drop messages if we don't know how to send them
+                return;
+            }
+
+            lock (peer)
+            {
+                // If we're negotiating a new epoch, queue data
+                if (peer.Epoch == 0 || peer.NextEpoch.State != HandshakeState.ExpectingHello)
+                {
+                    ByteSpan copyOfSpan = new byte[span.Length];
+                    span.CopyTo(copyOfSpan);
+
+                    peer.QueuedApplicationDataMessage.Add(copyOfSpan);
+                    return;
+                }
+
+                // Send any queued application data now
+                for (int ii = 0, nn = peer.QueuedApplicationDataMessage.Count; ii != nn; ++ii)
+                {
+                    ByteSpan queuedSpan = peer.QueuedApplicationDataMessage[ii];
+
+                    Record outgoingRecord = new Record();
+                    outgoingRecord.ContentType = ContentType.ApplicationData;
+                    outgoingRecord.Epoch = peer.Epoch;
+                    outgoingRecord.SequenceNumber = peer.CurrentEpoch.NextOutgoingSequence;
+                    outgoingRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(queuedSpan.Length);
+                    ++peer.CurrentEpoch.NextOutgoingSequence;
+
+                    // Encode the record to wire format
+                    ByteSpan packet = new byte[Record.Size + outgoingRecord.Length];
+                    ByteSpan writer = packet;
+                    outgoingRecord.Encode(writer);
+                    writer = writer.Slice(Record.Size);
+                    queuedSpan.CopyTo(writer);
+
+                    // Protect the record
+                    peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                        packet.Slice(Record.Size, outgoingRecord.Length)
+                        , packet.Slice(Record.Size, queuedSpan.Length)
+                        , ref outgoingRecord);
+
+                    base.QueueRawData(packet, remoteEndPoint);
+                }
+                peer.QueuedApplicationDataMessage.Clear();
+
+                {
+                    Record outgoingRecord = new Record();
+                    outgoingRecord.ContentType = ContentType.ApplicationData;
+                    outgoingRecord.Epoch = peer.Epoch;
+                    outgoingRecord.SequenceNumber = peer.CurrentEpoch.NextExpectedSequence;
+                    outgoingRecord.Length = (ushort)peer.CurrentEpoch.RecordProtection.GetEncryptedSize(span.Length);
+                    ++peer.CurrentEpoch.NextOutgoingSequence;
+
+                    // Encode the record to wire format
+                    ByteSpan packet = new byte[Record.Size + outgoingRecord.Length];
+                    ByteSpan writer = packet;
+                    outgoingRecord.Encode(writer);
+                    writer = writer.Slice(Record.Size);
+                    span.CopyTo(writer);
+
+                    // Protect the record
+                    peer.CurrentEpoch.RecordProtection.EncryptServerPlaintext(
+                        packet.Slice(Record.Size, outgoingRecord.Length)
+                        , packet.Slice(Record.Size, span.Length)
+                        , ref outgoingRecord
+                    );
+
+                    base.QueueRawData(packet, remoteEndPoint);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Allocate a new connection id
+        /// </summary>
+        private ConnectionId AllocateConnectionId()
+        {
+            ulong rawConnectionId = (ulong)Interlocked.Increment(ref this.connectionId_unsafe);
+            return ConnectionId.Create(rawConnectionId);
         }
     }
 }
index d9d8d0bf4a200433963f5da3943c0dc0cd2c211b..7ce3419e6232c5fdd1f70127beca0a57d78fcd83 100644 (file)
@@ -1,5 +1,54 @@
+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>
@@ -17,4 +66,434 @@ namespace Hazel.Dtls
     {
         NamedCurve = 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 ByteSpan Random;
+        public ByteSpan Cookie;
+        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)
+
+            + 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.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, ByteSpan span)
+        {
+            result = new ClientHello();
+            if (span.Length < MinSize)
+            {
+                return false;
+            }
+
+            ProtocolVersion clientVersion = (ProtocolVersion)span.ReadBigEndian16();
+            if (clientVersion != ProtocolVersion.DTLS1_2)
+            {
+                return false;
+            }
+            span = span.Slice(2);
+
+            result.Random = span.Slice(0, Dtls.Random.Size);
+            span = span.Slice(Dtls.Random.Size);
+
+            ///NOTE(mendsley): We ignore session id
+            byte sessionIdSize = span[0];
+            if (span.Length < 1 + sessionIdSize)
+            {
+                return false;
+            }
+            span = span.Slice(1 + sessionIdSize);
+
+            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;
+                }
+            }
+            span = span.Slice(1 + compressionMethodsSize);
+
+            if (!foundNullCompressionMethod)
+            {
+                return false;
+            }
+
+            // Parse extensions
+            while (span.Length > 0)
+            {
+                // Parse extension header
+                if (span.Length < 4)
+                {
+                    return false;
+                }
+
+                ExtensionType extensionType = (ExtensionType)span.ReadBigEndian16(0);
+                ushort extensionLength = span.ReadBigEndian16(2);
+                ByteSpan extensionData = span.Slice(4, extensionLength);
+                if (extensionData.Length < extensionLength)
+                {
+                    return false;
+                }
+
+                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)ProtocolVersion.DTLS1_2);
+            span = span.Slice(2);
+
+            Debug.Assert(this.Random.Length == Dtls.Random.Size);
+            this.Random.CopyTo(span);
+            span = span.Slice(Dtls.Random.Size);
+
+            // Do not encode session ids
+            span[0] = (byte)0;
+            span = span.Slice(1);
+
+            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);
+
+            // 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 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)
+            ;
+
+        /// <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)
+        {
+            ByteSpan cookie = ComputeAddressMac(peerAddress, hmac);
+
+            span.WriteBigEndian16((ushort)ProtocolVersion.DTLS1_2);
+            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)
+        {
+            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 ServerVersion;
+        public ByteSpan Random;
+        public CipherSuite CipherSuite;
+
+        public const int Size = 0
+            + 2 // server_version
+            + Dtls.Random.Size // random
+            + 1 // session_id (size)
+            + 2 // cipher_suite
+            + 1 // compression_method
+            ;
+
+        /// <summary>
+        /// Encode Handshake ServerHello to wire format
+        /// </summary>
+        public void Encode(ByteSpan span)
+        {
+            Debug.Assert(this.Random.Length == Dtls.Random.Size);
+
+            span.WriteBigEndian16((ushort)ProtocolVersion.DTLS1_2, 0);
+            span = span.Slice(2);
+
+            this.Random.CopyTo(span);
+            span = span.Slice(Dtls.Random.Size);
+
+            span[0] = 0;
+            span = span.Slice(1);
+
+            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;
+
+            ByteSpan result = new byte[totalSize];
+
+            ByteSpan writer = result;
+            writer.WriteBigEndian24((uint)certData.Length);
+            writer = writer.Slice(3);
+
+            certData.CopyTo(writer);
+            return result;
+        }
+    }
+
+    /// <summary>
+    /// Encode/decode Handshake Finished message
+    /// </summary>
+    public struct Finished
+    {
+        public const int Size = 12;
+    }
 }
index 1d70c6a9d31d6587c86a1c64a18fc332e1e9981b..cbee1b0f345b4c59bf0ae20e06d7830e0e08031d 100644 (file)
@@ -63,4 +63,22 @@ namespace Hazel.Dtls
         /// <returns>True if the input was authenticated and decrypted. Otherwise false</returns>
         bool DecryptCiphertextFromClient(ByteSpan output, ByteSpan input, ref Record record);
     }
+
+    /// <summary>
+    /// Factory to create record protection from cipher suite identifiers
+    /// </summary>
+    public sealed class RecordProtectionFactory
+    {
+        public static IRecordProtection Create(CipherSuite cipherSuite, ByteSpan masterSecret, ByteSpan serverRandom, ByteSpan clientRandom)
+        {
+            switch (cipherSuite)
+            {
+            case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+                return new Aes128GcmRecordProtection(masterSecret, serverRandom, clientRandom);
+
+            default:
+                return null;
+            }
+        }
+    }
 }
index 505f2d2debe205c7bb0da4df5bf386a55dc137cb..3e4fb594cee411eea0f11dd91d7a4efdf33e7846 100644 (file)
@@ -160,7 +160,7 @@ namespace Hazel.Dtls
         public void EncodeClientKeyExchangeMessage(ByteSpan output)
         {
             output[0] = (byte)X25519.KeySize;
-            X25519.Func(output.Slice(1), this.privateAgreementKey);
+            X25519.Func(output.Slice(1, X25519.KeySize), this.privateAgreementKey);
         }
 
         /// <inheritdoc />
index 5f1d51e80deaffe921d8a08560b9e07022969ea6..e091addffb377f7221e496bf47112b5893f31bd6 100644 (file)
@@ -75,6 +75,7 @@
     <Compile Include="ConnectionState.cs" />
     <Compile Include="Crypto\AesGcm.cs" />
     <Compile Include="Crypto\Const.cs" />
+    <Compile Include="Crypto\SpanCryptoExtensions.cs" />
     <Compile Include="Crypto\X25519.cs" />
     <Compile Include="DataReceivedEventArgs.cs" />
     <Compile Include="DisconnectedEventArgs.cs" />