/// <summary> /// Send (resend) a ClientHello message to the server /// </summary> private void SendClientHello() { // Reset our verification stream this.nextEpoch.VerificationStream.SetLength(0); this.nextEpoch.ClientRandom.FillWithRandom(this.random); // Describe our ClientHello flight ClientHello clientHello = new ClientHello(); clientHello.Random = this.nextEpoch.ClientRandom; clientHello.Cookie = this.nextEpoch.Cookie; clientHello.CipherSuites = new byte[2]; clientHello.CipherSuites.WriteBigEndian16((ushort)CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256); clientHello.SupportedCurves = new byte[2]; clientHello.SupportedCurves.WriteBigEndian16((ushort)NamedCurve.x25519); Handshake handshake = new Handshake(); handshake.MessageType = HandshakeType.ClientHello; handshake.Length = (uint)clientHello.CalculateSize(); handshake.MessageSequence = 0; handshake.FragmentOffset = 0; handshake.FragmentLength = handshake.Length; // Describe the record int plaintextLength = (int)(Handshake.Size + handshake.Length); Record outgoingRecord = new Record(); outgoingRecord.ContentType = ContentType.Handshake; outgoingRecord.Epoch = this.epoch; outgoingRecord.SequenceNumber = this.currentEpoch.NextOutgoingSequence; outgoingRecord.Length = (ushort)this.currentEpoch.RecordProtection.GetEncryptedSize(plaintextLength); ++this.currentEpoch.NextOutgoingSequence; // Convert the record to wire format ByteSpan packet = new byte[Record.Size + outgoingRecord.Length]; ByteSpan writer = packet; outgoingRecord.Encode(packet); writer = writer.Slice(Record.Size); handshake.Encode(writer); writer = writer.Slice(Handshake.Size); clientHello.Encode(writer); // Write ClientHello to the verification stream this.nextEpoch.VerificationStream.Write( packet.GetUnderlyingArray() , Record.Size , Handshake.Size + (int)handshake.Length ); // Protect the record this.currentEpoch.RecordProtection.EncryptClientPlaintext( packet.Slice(Record.Size, outgoingRecord.Length) , packet.Slice(Record.Size, plaintextLength) , ref outgoingRecord ); this.nextEpoch.State = HandshakeState.ExpectingServerHello; this.nextEpoch.NextPacketResendTime = DateTime.UtcNow + this.handshakeResendTimeout; base.WriteBytesToConnection(packet.GetUnderlyingArray(), packet.Length); }
/// <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> /// Process an incoming Handshake protocol message /// </summary> /// <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(ref Record record, ByteSpan message) { // Each record may have multiple Handshake messages while (message.Length > 0) { ByteSpan originalPayload = message; Handshake handshake; if (!Handshake.Parse(out handshake, message)) { this.logger.WriteError("Dropping malformed handshake message"); return(false); } message = message.Slice(Handshake.Size); if (message.Length < handshake.Length) { this.logger.WriteError($"Dropping malformed handshake message: AvailableBytes({message.Length}) Size({handshake.Length})"); return(false); } originalPayload = originalPayload.Slice(0, (int)(Handshake.Size + handshake.Length)); ByteSpan payload = originalPayload.Slice(Handshake.Size); message = message.Slice((int)handshake.Length); // We only support fragmented Certificate messages // from the server if (handshake.MessageType != HandshakeType.Certificate && (handshake.FragmentOffset != 0 || handshake.FragmentLength != handshake.Length)) { this.logger.WriteError($"Dropping fragmented handshake message Type({handshake.MessageType}) Offset({handshake.FragmentOffset}) FragmentLength({handshake.FragmentLength}) Length({handshake.Length})"); continue; } switch (handshake.MessageType) { case HandshakeType.HelloVerifyRequest: if (this.nextEpoch.State != HandshakeState.ExpectingServerHello) { this.logger.WriteError($"Dropping unexpected HelloVerifyRequest handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 0) { this.logger.WriteError($"Dropping bad-sequence HelloVerifyRequest MessageSequence({handshake.MessageSequence})"); continue; } HelloVerifyRequest helloVerifyRequest; if (!HelloVerifyRequest.Parse(out helloVerifyRequest, payload)) { this.logger.WriteError("Dropping malformed HelloVerifyRequest handshake message"); continue; } // Save the cookie this.nextEpoch.Cookie = new byte[helloVerifyRequest.Cookie.Length]; helloVerifyRequest.Cookie.CopyTo(this.nextEpoch.Cookie); // Restart the handshake this.SendClientHello(); break; case HandshakeType.ServerHello: if (this.nextEpoch.State != HandshakeState.ExpectingServerHello) { this.logger.WriteError($"Dropping unexpected ServerHello handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 1) { this.logger.WriteError($"Dropping bad-sequence ServerHello MessageSequence({handshake.MessageSequence})"); continue; } ServerHello serverHello; if (!ServerHello.Parse(out serverHello, payload)) { this.logger.WriteError("Dropping malformed ServerHello message"); continue; } switch (serverHello.CipherSuite) { case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: this.nextEpoch.Handshake = new X25519EcdheRsaSha256(this.random); break; default: this.logger.WriteError($"Dropping malformed ServerHello message. Unsupported CipherSuite({serverHello.CipherSuite})"); continue; } // Save server parameters this.nextEpoch.SelectedCipherSuite = serverHello.CipherSuite; serverHello.Random.CopyTo(this.nextEpoch.ServerRandom); this.nextEpoch.State = HandshakeState.ExpectingCertificate; this.nextEpoch.CertificateFragments.Clear(); this.nextEpoch.CertificatePayload = ByteSpan.Empty; // Append ServerHelllo message to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); break; case HandshakeType.Certificate: if (this.nextEpoch.State != HandshakeState.ExpectingCertificate) { this.logger.WriteError($"Dropping unexpected Certificate handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 2) { this.logger.WriteError($"Dropping bad-sequence Certificate MessageSequence({handshake.MessageSequence})"); continue; } // If this is a fragmented message if (handshake.FragmentLength != handshake.Length) { if (this.nextEpoch.CertificatePayload.Length != handshake.Length) { this.nextEpoch.CertificatePayload = new byte[handshake.Length]; this.nextEpoch.CertificateFragments.Clear(); } // Add this fragment payload.CopyTo(this.nextEpoch.CertificatePayload.Slice((int)handshake.FragmentOffset, (int)handshake.FragmentLength)); this.nextEpoch.CertificateFragments.Add(new FragmentRange { Offset = (int)handshake.FragmentOffset, Length = (int)handshake.FragmentLength }); this.nextEpoch.CertificateFragments.Sort((FragmentRange lhs, FragmentRange rhs) => { return(lhs.Offset.CompareTo(rhs.Offset)); }); // Have we completed the message? int currentOffset = 0; bool valid = true; foreach (FragmentRange range in this.nextEpoch.CertificateFragments) { if (range.Offset != currentOffset) { valid = false; break; } currentOffset += range.Length; } if (currentOffset != this.nextEpoch.CertificatePayload.Length) { valid = false; } // Still waiting on more fragments? if (!valid) { continue; } // Replace the message payload, and continue this.nextEpoch.CertificateFragments.Clear(); payload = this.nextEpoch.CertificatePayload; } X509Certificate2 certificate; if (!Certificate.Parse(out certificate, payload)) { this.logger.WriteError("Dropping malformed Certificate message"); continue; } // Verify the certificate is authenticate if (!this.serverCertificates.Contains(certificate)) { this.logger.WriteError("Dropping malformed Certificate message: Certificate not authentic"); continue; } RSA publicKey = certificate.PublicKey.Key as RSA; if (publicKey == null) { this.logger.WriteError("Dropping malfomed Certificate message: Certificate is not RSA signed"); continue; } // Add the final Certificate message to the verification stream Handshake fullCertificateHandhake = handshake; fullCertificateHandhake.FragmentOffset = 0; fullCertificateHandhake.FragmentLength = fullCertificateHandhake.Length; byte[] serializedCertificateHandshake = new byte[Handshake.Size]; fullCertificateHandhake.Encode(serializedCertificateHandshake); this.nextEpoch.VerificationStream.Write(serializedCertificateHandshake, 0, serializedCertificateHandshake.Length); this.nextEpoch.VerificationStream.Write(payload.GetUnderlyingArray(), payload.Offset, payload.Length); this.nextEpoch.ServerPublicKey = publicKey; this.nextEpoch.State = HandshakeState.ExpectingServerKeyExchange; break; case HandshakeType.ServerKeyExchange: if (this.nextEpoch.State != HandshakeState.ExpectingServerKeyExchange) { this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message State({this.nextEpoch.State})"); continue; } else if (this.nextEpoch.ServerPublicKey == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client Debug.Assert(false, "How are we processing a ServerKeyExchange message without a server public key?"); this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message: No server public key"); continue; } else if (this.nextEpoch.Handshake == null) { ///NOTE(mendsley): This _should_ not /// happen on a well-formed client Debug.Assert(false, "How did we receive a ServerKeyExchange message without a handshake instance?"); this.logger.WriteError($"Dropping unexpected ServerKeyExchange handshake message: No key agreement interface"); continue; } else if (handshake.MessageSequence != 3) { this.logger.WriteError($"Dropping bad-sequence ServerKeyExchange MessageSequence({handshake.MessageSequence})"); continue; } ByteSpan sharedSecret = new byte[this.nextEpoch.Handshake.SharedKeySize()]; if (!this.nextEpoch.Handshake.VerifyServerMessageAndGenerateSharedKey(sharedSecret, payload, this.nextEpoch.ServerPublicKey)) { this.logger.WriteError("Dropping malformed ServerKeyExchangeMessage"); return(false); } // Generate the session master secret ByteSpan randomSeed = new byte[2 * Random.Size]; this.nextEpoch.ClientRandom.CopyTo(randomSeed); this.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 record protection for the upcoming epoch switch (this.nextEpoch.SelectedCipherSuite) { case CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: this.nextEpoch.RecordProtection = new Aes128GcmRecordProtection( masterSecret , this.nextEpoch.ServerRandom , this.nextEpoch.ClientRandom ); break; default: ///NOTE(mendsley): this _should_ not /// happen on a well-formed client. Debug.Assert(false, "SeverHello processing already approved this ciphersuite"); this.logger.WriteError($"Dropping malformed ServerKeyExchangeMessage: Could not create record protection"); return(false); } this.nextEpoch.State = HandshakeState.ExpectingServerHelloDone; this.nextEpoch.MasterSecret = masterSecret; // Append ServerKeyExchange to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); break; case HandshakeType.ServerHelloDone: if (this.nextEpoch.State != HandshakeState.ExpectingServerHelloDone) { this.logger.WriteError($"Dropping unexpected ServerHelloDone handshake message State({this.nextEpoch.State})"); continue; } else if (handshake.MessageSequence != 4) { this.logger.WriteError($"Dropping bad-sequence ServerHelloDone MessageSequence({handshake.MessageSequence})"); continue; } this.nextEpoch.State = HandshakeState.ExpectingChangeCipherSpec; // Append ServerHelloDone to the verification stream this.nextEpoch.VerificationStream.Write( originalPayload.GetUnderlyingArray() , originalPayload.Offset , originalPayload.Length ); this.SendClientKeyExchangeFlight(false); break; case HandshakeType.Finished: if (this.nextEpoch.State != HandshakeState.ExpectingFinished) { this.logger.WriteError($"Dropping unexpected Finished handshake message State({this.nextEpoch.State})"); continue; } else if (payload.Length != Finished.Size) { this.logger.WriteError($"Dropping malformed Finished handshake message Size({payload.Length})"); continue; } else if (handshake.MessageSequence != 7) { this.logger.WriteError($"Dropping bad-sequence Finished MessageSequence({handshake.MessageSequence})"); continue; } // Verify the digest from the server if (1 != Crypto.Const.ConstantCompareSpans(payload, this.nextEpoch.ServerVerification)) { this.logger.WriteError("Dropping non-verified Finished handshake message"); return(false); } ++this.nextEpoch.Epoch; this.nextEpoch.State = HandshakeState.Established; this.nextEpoch.NextPacketResendTime = DateTime.MinValue; this.nextEpoch.ServerVerification.SecureClear(); this.nextEpoch.MasterSecret.SecureClear(); this.FlushQueuedApplicationData(); break; // Drop messages we do not support case HandshakeType.CertificateRequest: case HandshakeType.HelloRequest: this.logger.WriteError($"Dropping unsupported handshake message MessageType({handshake.MessageType})"); break; // Drop messages that originate from the client case HandshakeType.ClientHello: case HandshakeType.ClientKeyExchange: case HandshakeType.CertificateVerify: this.logger.WriteError($"Dropping client handshake message MessageType({handshake.MessageType})"); break; } } return(true); }