/// <summary> /// Send (resend) the ClientKeyExchange flight /// </summary> /// <param name="isRetransmit"> /// True if this is a retransmit of the flight. Otherwise, /// false /// </param> private void SendClientKeyExchangeFlight(bool isRetransmit) { // Describe our flight Handshake keyExchangeHandshake = new Handshake(); keyExchangeHandshake.MessageType = HandshakeType.ClientKeyExchange; keyExchangeHandshake.Length = (ushort)this.nextEpoch.Handshake.CalculateClientMessageSize(); keyExchangeHandshake.MessageSequence = 5; keyExchangeHandshake.FragmentOffset = 0; keyExchangeHandshake.FragmentLength = keyExchangeHandshake.Length; Record keyExchangeRecord = new Record(); keyExchangeRecord.ContentType = ContentType.Handshake; keyExchangeRecord.Epoch = this.epoch; keyExchangeRecord.SequenceNumber = this.currentEpoch.NextOutgoingSequence; keyExchangeRecord.Length = (ushort)this.currentEpoch.RecordProtection.GetEncryptedSize(Handshake.Size + (int)keyExchangeHandshake.Length); ++this.currentEpoch.NextOutgoingSequence; Record changeCipherSpecRecord = new Record(); changeCipherSpecRecord.ContentType = ContentType.ChangeCipherSpec; changeCipherSpecRecord.Epoch = this.epoch; changeCipherSpecRecord.SequenceNumber = this.currentEpoch.NextOutgoingSequence; changeCipherSpecRecord.Length = (ushort)this.currentEpoch.RecordProtection.GetEncryptedSize(ChangeCipherSpec.Size); ++this.currentEpoch.NextOutgoingSequence; Handshake finishedHandshake = new Handshake(); finishedHandshake.MessageType = HandshakeType.Finished; finishedHandshake.Length = Finished.Size; finishedHandshake.MessageSequence = 6; finishedHandshake.FragmentOffset = 0; finishedHandshake.FragmentLength = finishedHandshake.Length; Record finishedRecord = new Record(); finishedRecord.ContentType = ContentType.Handshake; finishedRecord.Epoch = this.nextEpoch.Epoch; finishedRecord.SequenceNumber = this.nextEpoch.NextOutgoingSequence; finishedRecord.Length = (ushort)this.nextEpoch.RecordProtection.GetEncryptedSize(Handshake.Size + (int)finishedHandshake.Length); ++this.nextEpoch.NextOutgoingSequence; // Encode flight to wire format int packetLength = 0 + Record.Size + keyExchangeRecord.Length + Record.Size + changeCipherSpecRecord.Length + Record.Size + finishedRecord.Length; ; ByteSpan packet = new byte[packetLength]; ByteSpan writer = packet; keyExchangeRecord.Encode(writer); writer = writer.Slice(Record.Size); keyExchangeHandshake.Encode(writer); writer = writer.Slice(Handshake.Size); this.nextEpoch.Handshake.EncodeClientKeyExchangeMessage(writer); ByteSpan startOfChangeCipherSpecRecord = packet.Slice(Record.Size + keyExchangeRecord.Length); writer = startOfChangeCipherSpecRecord; changeCipherSpecRecord.Encode(writer); writer = writer.Slice(Record.Size); ChangeCipherSpec.Encode(writer); writer = writer.Slice(ChangeCipherSpec.Size); ByteSpan startOfFinishedRecord = startOfChangeCipherSpecRecord.Slice(Record.Size + changeCipherSpecRecord.Length); writer = startOfFinishedRecord; finishedRecord.Encode(writer); writer = writer.Slice(Record.Size); finishedHandshake.Encode(writer); writer = writer.Slice(Handshake.Size); // Interject here to writer our client key exchange // message into the verification stream if (!isRetransmit) { this.nextEpoch.VerificationStream.Write( packet.GetUnderlyingArray() , Record.Size , Handshake.Size + (int)keyExchangeHandshake.Length ); } // Calculate the hash of the verification stream ByteSpan handshakeHash; using (SHA256 sha256 = SHA256.Create()) { this.nextEpoch.VerificationStream.Position = 0; handshakeHash = sha256.ComputeHash(this.nextEpoch.VerificationStream); } // Expand our master secret into Finished digests for the client and server PrfSha256.ExpandSecret( this.nextEpoch.ServerVerification , this.nextEpoch.MasterSecret , PrfLabel.SERVER_FINISHED , handshakeHash ); PrfSha256.ExpandSecret( writer.Slice(0, Finished.Size) , this.nextEpoch.MasterSecret , PrfLabel.CLIENT_FINISHED , handshakeHash ); writer = writer.Slice(Finished.Size); // Protect the ClientKeyExchange record this.currentEpoch.RecordProtection.EncryptClientPlaintext( packet.Slice(Record.Size, keyExchangeRecord.Length) , packet.Slice(Record.Size, Handshake.Size + (int)keyExchangeHandshake.Length) , ref keyExchangeRecord ); // Protect the ChangeCipherSpec record this.currentEpoch.RecordProtection.EncryptClientPlaintext( startOfChangeCipherSpecRecord.Slice(Record.Size, changeCipherSpecRecord.Length) , startOfChangeCipherSpecRecord.Slice(Record.Size, ChangeCipherSpec.Size) , ref changeCipherSpecRecord ); // Protect the Finished record this.nextEpoch.RecordProtection.EncryptClientPlaintext( startOfFinishedRecord.Slice(Record.Size, finishedRecord.Length) , startOfFinishedRecord.Slice(Record.Size, Handshake.Size + (int)finishedHandshake.Length) , ref finishedRecord ); this.nextEpoch.State = HandshakeState.ExpectingChangeCipherSpec; this.nextEpoch.NextPacketResendTime = DateTime.UtcNow + this.handshakeResendTimeout; base.WriteBytesToConnection(packet.GetUnderlyingArray(), packet.Length); }
/// <summary> /// Handle an incoming datagram /// </summary> /// <param name="span">Bytes of the datagram</param> private void HandleReceive(ByteSpan span) { // Each incoming packet may contain multiple DTLS // records while (span.Length > 0) { Record record; if (!Record.Parse(out record, span)) { this.logger.WriteError("Dropping malformed record"); return; } span = span.Slice(Record.Size); if (span.Length < record.Length) { this.logger.WriteError($"Dropping malformed record. Length({record.Length}) Available Bytes({span.Length})"); return; } ByteSpan recordPayload = span.Slice(0, record.Length); span = span.Slice(record.Length); // Early out and drop ApplicationData records if (record.ContentType == ContentType.ApplicationData && this.nextEpoch.State != HandshakeState.Established) { this.logger.WriteError("Dropping ApplicationData record. Cannot process yet"); continue; } // Drop records from a different epoch if (record.Epoch != this.epoch) { this.logger.WriteError($"Dropping bad-epoch record. RecordEpoch({record.Epoch}) Epoch({this.epoch})"); continue; } // Prevent replay attacks by dropping records // we've already processed int windowIndex = (int)(this.currentEpoch.NextExpectedSequence - record.SequenceNumber - 1); ulong windowMask = 1ul << windowIndex; if (record.SequenceNumber < this.currentEpoch.NextExpectedSequence) { if (windowIndex >= 64) { this.logger.WriteError($"Dropping too-old record: Sequnce({record.SequenceNumber}) Expected({this.currentEpoch.NextExpectedSequence})"); continue; } if ((this.currentEpoch.PreviousSequenceWindowBitmask & windowMask) != 0) { this.logger.WriteError("Dropping duplicate record"); continue; } } // Verify record authenticity int decryptedSize = this.currentEpoch.RecordProtection.GetDecryptedSize(recordPayload.Length); ByteSpan decryptedPayload = recordPayload.ReuseSpanIfPossible(decryptedSize); if (!this.currentEpoch.RecordProtection.DecryptCiphertextFromServer(decryptedPayload, recordPayload, ref record)) { this.logger.WriteError("Dropping non-authentic record"); return; } recordPayload = decryptedPayload; // Update out sequence number bookkeeping if (record.SequenceNumber >= this.currentEpoch.NextExpectedSequence) { int windowShift = (int)(record.SequenceNumber + 1 - this.currentEpoch.NextExpectedSequence); this.currentEpoch.PreviousSequenceWindowBitmask <<= windowShift; this.currentEpoch.NextExpectedSequence = record.SequenceNumber + 1; } else { this.currentEpoch.PreviousSequenceWindowBitmask |= windowMask; } switch (record.ContentType) { case ContentType.ChangeCipherSpec: if (this.nextEpoch.State != HandshakeState.ExpectingChangeCipherSpec) { this.logger.WriteError($"Dropping unexpected ChangeCipherSpec State({this.nextEpoch.State})"); break; } else if (this.nextEpoch.RecordProtection == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client. Debug.Assert(false, "How did we receive a ChangeCipherSpec message without a pending record protection instance?"); break; } if (!ChangeCipherSpec.Parse(recordPayload)) { this.logger.WriteError("Dropping malformed ChangeCipherSpec message"); break; } // Migrate to the next epoch this.epoch = this.nextEpoch.Epoch; this.currentEpoch.RecordProtection = this.nextEpoch.RecordProtection; this.currentEpoch.NextOutgoingSequence = this.nextEpoch.NextOutgoingSequence; this.currentEpoch.NextExpectedSequence = 1; this.currentEpoch.PreviousSequenceWindowBitmask = 0; this.nextEpoch.State = HandshakeState.ExpectingFinished; this.nextEpoch.SelectedCipherSuite = CipherSuite.TLS_NULL_WITH_NULL_NULL; this.nextEpoch.RecordProtection = null; this.nextEpoch.Handshake?.Dispose(); this.nextEpoch.Cookie = ByteSpan.Empty; this.nextEpoch.VerificationStream.SetLength(0); this.nextEpoch.ServerPublicKey = null; this.nextEpoch.ServerRandom.SecureClear(); this.nextEpoch.ClientRandom.SecureClear(); this.nextEpoch.MasterSecret.SecureClear(); break; case ContentType.Alert: this.logger.WriteError("Dropping unsupported alert record"); continue; case ContentType.Handshake: if (!ProcessHandshake(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.HandleReceive(reader, recordPayload.Length); break; } } }