+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
{
/// <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>
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);
}
}
}
+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>
{
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;
+ }
}