/* * Secucore * * Copyright (C) 2023 Trevor Hall * All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. * */ using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; using SecuCore.Curves; using SecuCore.Security; using SecuCore.Shared; using SecuCore.TLS.Exceptions; using static SecuCore.KeyDerivation; using static SecuCore.TLS.CipherSuites; using static SecuCore.TLS.TLSData; namespace SecuCore.TLS { public class TLSRecordLayer { public static TLSVersion preferredVersion = TLSVersion.TLS12; public static string preferredNamedCurve = "x25519"; public static int preferredFragmentSize = 8192; public static bool verifyServerSignature = true; public static ushort[] AllImplementedCipherSuites = CipherSuites.GetSupportedCipherSuites(); //for handshake process X509Cert remoteServerCertificate; X509CertChain serverCertChain; KeyExchangeInfo serverKeyExchangeInfo; TLSCipherSuite cipherSuite; ulong sessionTicketLifetime = 0; byte[] sessionTicket = null; byte[] tlsEncryptedPremaster; byte[] tlsPremasterSecret; byte[] tlsMasterSecret; byte[] clientRandom; byte[] serverRandom; //for the key exchange byte[] clientPrivateKey; byte[] clientPublicKey; //for encryption and decryption KeyExpansionResult keyRing; IDataController dataController; TLSVersion vers; private string lastErrorString; private byte[] versionb; private SecRNG srng = new SecRNG(); public delegate bool ValidateServerCertificate(X509CertChain serverCertificates, bool handshakeSignatureValid); public delegate X509Cert ClientCertificateRequest(); private ValidateServerCertificate certificateValidationCallback = null; private ClientCertificateRequest clientCertificateRequestCallback = null; private TLSEncryptionProvider tlsCrypt = null; private AsyncAutoResetEvent negotiationCompleted = new AsyncAutoResetEvent(); public bool isConnected = false; public bool isFaulted = false; private bool trackHandshakes = true; private byte[] hsbuf; private int hsbufpos = 0; private bool sendEncrypted = false; private bool recieveEncrypted = false; ulong seq_local = 0; ulong seq_server = 0; private int AvailableApplicationDataBytes = 0; private byte[] AvailableApplicationData = null; private bool serverHelloDoneRecieved = false; private bool serverFinishedRecieved = false; private const bool use_extended_master_secret = false; private bool verify_server_signature = true; private bool shutdownRequested = false; public void Dispose() { shutdownRequested = true; negotiationCompleted.Set(); if (tlsCrypt != null) tlsCrypt.Dispose(); if (remoteServerCertificate != null) remoteServerCertificate.Dispose(); if (serverCertChain != null) serverCertChain.Dispose(); sessionTicket = null; tlsMasterSecret = null; clientRandom = null; serverRandom = null; clientPrivateKey = null; clientPublicKey = null; versionb = null; hsbuf = null; AvailableApplicationData = null; AvailableApplicationDataBytes = 0; dataController = null; } public TLSRecordLayer(IDataController _dataController) { vers = preferredVersion; this.dataController = _dataController; this.versionb = GetVersionBytes(vers); } public TLSRecordLayer(IDataController _dataController, TLSVersion version = TLSVersion.TLS12) { vers = version; this.dataController = _dataController; this.versionb = GetVersionBytes(version); } public async ValueTask EstablishTLSConnection(string hostname, ValidateServerCertificate ValidateServerCertificateCallback, ClientCertificateRequest CertificateRequestCallback) { this.certificateValidationCallback = ValidateServerCertificateCallback; this.clientCertificateRequestCallback = CertificateRequestCallback; try { _ = HandshakeProcess(hostname); await negotiationCompleted.WaitAsync().ConfigureAwait(false); return isConnected; } catch (Exception e) { lastErrorString = e.Message; isFaulted = true; } return false; } private async Task HandshakeProcess(string hostname) { try { StartLoggingHandshakes(); HandshakeRecord clienthello = CreateClientHello(hostname); SendHandshakeRecord(clienthello); FlushOutput(); while (!serverHelloDoneRecieved) { if (shutdownRequested) return; if (!await ReadOne().ConfigureAwait(false)) return; } HandshakeRecord clientkex; if(tlsEncryptedPremaster != null) clientkex = CreateClientPremasterExchange(); else clientkex = CreateClientKeyExchange(); SendHandshakeRecord(clientkex); FlushOutput(); TLSRecord changeCipherSpec = GetClientChangeCipherSpec(); ScheduleSend(changeCipherSpec); FlushOutput(); HandshakeRecord clientfinished = CreateClientFinishedRecord(); SendHandshakeRecord(clientfinished); FlushOutput(); while (!serverFinishedRecieved) { if (shutdownRequested) return; if (!await ReadOne().ConfigureAwait(false)) return; } isConnected = true; negotiationCompleted.Set(); } catch (Exception e) { isFaulted = true; lastErrorString = e.Message; negotiationCompleted.Set(); } } private HandshakeRecord CreateClientKeyExchange() { int pubkeylen = this.clientPublicKey.Length; byte[] publicKeyData = new byte[pubkeylen + 1]; publicKeyData[0] = (byte)pubkeylen; Buffer.BlockCopy(this.clientPublicKey, 0, publicKeyData, 1, pubkeylen); return new HandshakeRecord() { Type = HandshakeRecord.HS_Client_KeyExchange, Length = (uint)publicKeyData.Length, Data = publicKeyData }; } private HandshakeRecord CreateClientPremasterExchange() { byte[] encryptedPremasterPlusLength = new byte[tlsEncryptedPremaster.Length + 2]; encryptedPremasterPlusLength[0] = (byte)((tlsEncryptedPremaster.Length >> 8) & 0xff); encryptedPremasterPlusLength[1] = (byte)(tlsEncryptedPremaster.Length & 0xff); Buffer.BlockCopy(tlsEncryptedPremaster, 0, encryptedPremasterPlusLength, 2, tlsEncryptedPremaster.Length); return new HandshakeRecord() { Type = HandshakeRecord.HS_Client_KeyExchange, Length = (uint)encryptedPremasterPlusLength.Length, Data = encryptedPremasterPlusLength }; } private HandshakeRecord CreateClientHello(string externalHost) { byte[] cliRandom = new byte[32]; byte[] cliTime = GetUnixTime(); Buffer.BlockCopy(cliTime, 0, cliRandom, 0, 4); srng.GetRandomBytes(cliRandom, 4, 28); this.clientRandom = cliRandom; byte[] cipherSuite = GetCipherSuite(AllImplementedCipherSuites); byte[] compressionMethods = new byte[] { 0x01, 0x00 }; byte[] extensions = GetExtensions(externalHost, use_extended_master_secret, preferredNamedCurve); byte[] hellodata = Tools.JoinAll(versionb, cliRandom, new byte[1] {0}, cipherSuite, compressionMethods, extensions); HandshakeRecord hsr = new HandshakeRecord() { Type = HandshakeRecord.HS_Client_Hello, Length = (uint)hellodata.Length, Data = hellodata }; return hsr; } private HandshakeRecord CreateClientFinishedRecord() { //create verification data byte[] allshakeshash = GetFinishedHash(); using(HMAC hm = cipherSuite.CreateHMAC(tlsMasterSecret)) { byte[] verifyDat = null; if(vers == TLSVersion.TLS12) { verifyDat = GenerateVerifyData(TLSData.label_clientfinished, allshakeshash, hm); } else { verifyDat = GenerateVerifyData11(TLSData.label_clientfinished, allshakeshash, tlsMasterSecret); } return new HandshakeRecord() { Type = HandshakeRecord.HS_Finished, Length = (uint)verifyDat.Length, Data = verifyDat }; } } private void SendHandshakeRecord(HandshakeRecord hsr) { UpdateHandshakes(hsr); TLSRecord tlsr = new TLSRecord(hsr, vers); ScheduleSend(tlsr); } public Queue handshakeRecords = new Queue(); private async ValueTask ReadOne() { if (shutdownRequested) return false; try { if(handshakeRecords.Count == 0) { TLSRecord tlsr = await ReadNextRecord().ConfigureAwait(false); if(tlsr.Type == TLSRecord.TLSR_Handshake) { //Parse them here byte[] hsdat = tlsr.Data; int idx = 0; int remain = hsdat.Length; while(remain > 0) { byte[] header = new byte[4]; Buffer.BlockCopy(hsdat, idx, header, 0, 4); idx += 4; int hskl = (header[1] << 16 | header[2] << 8 | header[3]); if (hskl > remain) throw new TLSHandshakeException("Error parsing handshake messages"); byte[] fragment = new byte[hskl]; Buffer.BlockCopy(hsdat, idx, fragment, 0, hskl); idx += hskl; remain -= hskl + 4; HandshakeRecord shk = HandshakeRecord.Parse(header, fragment); handshakeRecords.Enqueue(shk); } } else if (tlsr.Type == TLSRecord.TLSR_ChangeSipherSpec) { recieveEncrypted = true; } } if(handshakeRecords.TryDequeue(out HandshakeRecord hsr)) { if (hsr.Type == HandshakeRecord.HS_Server_Hello) { ProcessServerHello(hsr); } else if (hsr.Type == HandshakeRecord.HS_Server_KeyExchange) { ProcessServerKeyExchange(hsr); } else if (hsr.Type == HandshakeRecord.HS_Certificate) { ProcessServerCertificate(hsr); } else if (hsr.Type == HandshakeRecord.HS_Server_HelloDone) { ProcessServerHelloDone(hsr); } else if (hsr.Type == HandshakeRecord.HS_NewSessionTicket) { ProcessServerNewSessionTicket(hsr); } else if (hsr.Type == HandshakeRecord.HS_Finished) { ProcessServerFinished(hsr); } UpdateHandshakes(hsr); } return true; } catch (Exception ex) { lastErrorString = ex.Message; negotiationCompleted.Set(); } return false; } private void ProcessServerHello(HandshakeRecord hsr) { ServerHello svh = ServerHello.Parse(hsr); serverRandom = svh.serverRandom; if (!preferredCipherSuites.Contains(svh.selectedCipher)) throw new TLSHandshakeException("Server attempting to use unsupported Ciphersuite"); cipherSuite = InitializeCipherSuite(svh.selectedCipher); } private void ProcessServerKeyExchange(HandshakeRecord hsr) { serverKeyExchangeInfo = KeyExchangeInfo.Parse(hsr.Data, vers); } private void ProcessServerCertificate(HandshakeRecord hsr) { if (!ParseServerCertificates(hsr)) throw new TLSHandshakeException("Unable to acquire server certificate"); } private void ProcessServerHelloDone(HandshakeRecord hsr) { bool sigVerified = true; if (verify_server_signature) { if (cipherSuite.tlsparams.keyExchangeAlgorithm != KeyExchangeAlgorithm.RSA) sigVerified = VerifyServerSignature(); if (certificateValidationCallback != null) { if (!certificateValidationCallback.Invoke(serverCertChain, sigVerified)) throw new TLSValidationException("Parent client rejected server certificate"); } } if (!CreateKeys()) throw new TLSEncryptionException("Failed to create encryption keys"); serverHelloDoneRecieved = true; } private void ProcessServerNewSessionTicket(HandshakeRecord hsr) { uint lifetime = 0; lifetime |= (uint)(hsr.Data[0] << 24); lifetime |= (uint)(hsr.Data[1] << 16); lifetime |= (uint)(hsr.Data[2] << 8); lifetime |= (uint)(hsr.Data[3]); ushort ticketlen = 0; ticketlen |= (ushort)(hsr.Data[4] << 8); ticketlen |= (ushort)(hsr.Data[5]); if (ticketlen > 8192) throw new TLSProtocolException("Session Ticket exceeds internal limit"); byte[] sessticket = new byte[ticketlen]; Buffer.BlockCopy(hsr.Data, 6, sessticket, 0, ticketlen); this.sessionTicket = sessticket; this.sessionTicketLifetime = lifetime; } private void ProcessServerFinished(HandshakeRecord hsr) { byte[] verifyData = hsr.Data; //create verification data byte[] allshakeshash = GetFinishedHash(); using(HMAC hm = cipherSuite.CreateHMAC(tlsMasterSecret)) { byte[] expectedverifyDat = null; if(vers == TLSVersion.TLS12) { expectedverifyDat = GenerateVerifyData(TLSData.label_serverfinished, allshakeshash, hm); } else { expectedverifyDat = GenerateVerifyData11(TLSData.label_serverfinished, allshakeshash, tlsMasterSecret); } if (!Tools.ArraysEqual(verifyData, expectedverifyDat)) throw new TLSHandshakeException("Failed to verify server finished message"); FinishLoggingHandshakes(); serverFinishedRecieved = true; } } private bool CreateKeys() { try { byte[] premaster = null; KeyExchangeAlgorithm exch = cipherSuite.tlsparams.keyExchangeAlgorithm; if (exch == KeyExchangeAlgorithm.RSA) { //generate random premaster secret premaster = new byte[48]; premaster[0] = versionb[0]; premaster[1] = versionb[1]; srng.GetRandomBytes(premaster, 2, 46); RSAPublicKey rpk = remoteServerCertificate.GetRSAPublicKey(); tlsEncryptedPremaster = rpk.EncryptData(premaster); } else if (exch == KeyExchangeAlgorithm.ECDHE_RSA || exch == KeyExchangeAlgorithm.ECDHE_ECDSA || exch == KeyExchangeAlgorithm.ECDHE_PSK) { //first generate our keys for the key exchange, the public key will be sent on 'client key exchange' GenerateClientKeys(preferredNamedCurve); premaster = CalculateSharedSecret(clientPrivateKey, serverKeyExchangeInfo.publicKey, preferredNamedCurve); } else { //unsupported throw new TLSProtocolException("Unsupported key exchange algorithm"); } byte[] master = null; tlsPremasterSecret = premaster; //generate master secret if(vers == TLSVersion.TLS12) { using (HMAC hm = cipherSuite.CreateHMAC(premaster)) { master = GenerateMasterSecret(use_extended_master_secret, clientRandom, serverRandom, hm); } tlsMasterSecret = master; using (HMAC hm = cipherSuite.CreateHMAC(tlsMasterSecret)) { this.keyRing = PerformKeyExpansion(master, clientRandom, serverRandom, cipherSuite.tlsparams.HashSize, cipherSuite.tlsparams.BulkKeySize, cipherSuite.tlsparams.BulkIVSize, hm); } return true; } else if (vers == TLSVersion.TLS11) { master = GenerateMasterSecret11(premaster, clientRandom, serverRandom); tlsMasterSecret = master; this.keyRing = PerformKeyExpansionTLS11(master, clientRandom, serverRandom, cipherSuite.tlsparams.HashSize, cipherSuite.tlsparams.BulkKeySize, cipherSuite.tlsparams.BulkIVSize); return true; } else { return false; } } catch (Exception) { return false; } } private void GenerateClientKeys(string curvename) { if(curvename == "x25519") { byte[] prirnd = new byte[32]; srng.GetRandomBytes(prirnd, 0, 32); byte[] clipri = Curve25519.ClampPrivateKey(prirnd); byte[] clipub = Curve25519.GetPublicKey(clipri); clientPrivateKey = prirnd; clientPublicKey = clipub; return; } //define curve ECCurve curve = ECCurve.CreateFromFriendlyName(curvename); //generate master secret using (ECDiffieHellman clientECDH = ECDiffieHellman.Create(curve)) { ECParameters ecp = clientECDH.ExportParameters(true); byte[] privateKey = ecp.D; byte[] Qx = ecp.Q.X; byte[] Qy = ecp.Q.Y; byte[] cprivatekey = new byte[1 + privateKey.Length]; cprivatekey[0] = 0x04; Buffer.BlockCopy(privateKey, 0, cprivatekey, 1, privateKey.Length); byte[] cpublicKey = new byte[1 + Qx.Length + Qy.Length]; cpublicKey[0] = 0x04; Buffer.BlockCopy(Qx, 0, cpublicKey, 1, Qx.Length); Buffer.BlockCopy(Qy, 0, cpublicKey, 1 + Qx.Length, Qy.Length); clientPrivateKey = cprivatekey; clientPublicKey = cpublicKey; } } private bool ParseServerCertificates(HandshakeRecord hsr) { int certificatesLen = 0; certificatesLen |= hsr.Data[0] << 16; certificatesLen |= hsr.Data[1] << 8; certificatesLen |= hsr.Data[2]; byte[] certificatesData = new byte[certificatesLen]; Buffer.BlockCopy(hsr.Data, 3, certificatesData, 0, certificatesLen); X509CertChain certchn = new X509CertChain(); int bufp = 0; int idx = 0; while (bufp < certificatesLen) { int nextCertLen = 0; nextCertLen |= certificatesData[bufp++] << 16; nextCertLen |= certificatesData[bufp++] << 8; nextCertLen |= certificatesData[bufp++]; if (nextCertLen > TLSData.CertificateLengthLimit) throw new TLSProtocolException("Certificate[" + idx + "] exceeds bounds"); byte[] certDat = new byte[nextCertLen]; Buffer.BlockCopy(certificatesData, bufp, certDat, 0, nextCertLen); bufp += nextCertLen; X509Cert cert = Security.X509Cert.FromASN(certDat, 0); certchn.AddLink(cert); if (idx == 0) { remoteServerCertificate = cert; } idx++; } if (remoteServerCertificate != null) { this.serverCertChain = certchn; return true; } return false; } private bool VerifyServerSignature() { try { byte[] pms = clientRandom.Concat(serverRandom).Concat(serverKeyExchangeInfo.edchParams).ToArray(); byte[] sig = serverKeyExchangeInfo.signature; if(vers == TLSVersion.TLS12) { using (HashAlgorithm ha = cipherSuite.GetHasher()) { byte[] dataHash = ha.ComputeHash(pms); return cipherSuite.VerifyHash(dataHash, sig, remoteServerCertificate); } } else { using (MD5 md5 = new MD5CryptoServiceProvider()) { using(SHA1 sha1 = new SHA1CryptoServiceProvider()) { byte[] md5Hash = md5.ComputeHash(pms); byte[] sha1Hash = sha1.ComputeHash(pms); byte[] hash = new byte[md5Hash.Length + sha1Hash.Length]; Buffer.BlockCopy(md5Hash, 0, hash, 0, md5Hash.Length); Buffer.BlockCopy(sha1Hash, 0, hash, md5Hash.Length, sha1Hash.Length); return cipherSuite.VerifyHash(hash, sig, remoteServerCertificate); } } } } catch (Exception) { return false; } } private TLSRecord GetClientChangeCipherSpec() { TLSRecord tlsr = new TLSRecord() { Type = 0x14, Version = (ushort)(versionb[0] << 8 | versionb[1]), Length = (ushort)0x01, Data = new byte[] { 0x01 } }; return tlsr; } private void StartLoggingHandshakes() { hsbuf = new byte[8192]; hsbufpos = 0; trackHandshakes = true; } private void FinishLoggingHandshakes() { hsbuf = null; hsbufpos = 0; trackHandshakes = false; } private void UpdateHandshakes(HandshakeRecord hsr) { if (!trackHandshakes) return; byte[] hshk = hsr.Serialize(); if (hsbufpos + hshk.Length > hsbuf.Length) { //this almost never happen byte[] rsz = new byte[hsbuf.Length + 4096]; Buffer.BlockCopy(hsbuf, 0, rsz, 0, hsbufpos); hsbuf = rsz; } Buffer.BlockCopy(hshk, 0, hsbuf, hsbufpos, hshk.Length); hsbufpos += hshk.Length; } private byte[] GetFinishedHash() { byte[] hshdat = Tools.SubArray(hsbuf, 0, hsbufpos); if(vers == TLSVersion.TLS12) { using (HashAlgorithm ha = SHA256.Create()) { byte[] hshhash = ha.ComputeHash(hshdat); return hshhash; } } else { byte[] hash = new byte[36]; using (MD5 md5 = MD5.Create()) { byte[] hshmd5 = md5.ComputeHash(hshdat); Buffer.BlockCopy(hshmd5, 0, hash, 0, 16); } using (SHA1 sha1 = SHA1.Create()) { byte[] hshsha1 = sha1.ComputeHash(hshdat); Buffer.BlockCopy(hshsha1, 0, hash, 16, 20); } return hash; } } private void InitializeEncryption() { tlsCrypt = cipherSuite.InitializeEncryption(this.keyRing); } private void ScheduleSend(TLSRecord tlso, TaskCompletionSource _tcs = null) { if (shutdownRequested) return; if (sendEncrypted) { tlso = Encryption.EncryptRecord(tlso, tlsCrypt, seq_local); seq_local++; } if (tlso.Type == TLSRecord.TLSR_ChangeSipherSpec) { //we are changing to an encrypted state InitializeEncryption(); sendEncrypted = true; seq_local = 0; } dataController.QueueData(new DataDispatch() { data = tlso.Serialize(), tcs = _tcs }); } private void FlushOutput() { dataController.FlushData(); } private ValueTask AskForData(int len) { TaskCompletionSource dtcs = new TaskCompletionSource(); DataRequest dr = new DataRequest() { length = len, tcs = dtcs }; dataController.RequestData(dr); return new ValueTask(dtcs.Task); } private async ValueTask ReadNextRecord() { byte[] header = await AskForData(5).ConfigureAwait(false); if (header.Length != 5) throw new TLSNetworkException("Failed while reading TLS record"); byte _type = header[0]; ushort _version = (ushort)(header[2] | (header[1] << 8)); ushort _recordLength = (ushort)(header[4] | (header[3] << 8)); if (_recordLength > TLSData.RecordLengthLimit) { //throw new TLSProtocolException("Record exceeds length limits"); } byte[] _data = await AskForData(_recordLength).ConfigureAwait(false); if (_type == 0x15) { ushort _alertLen = (ushort)(header[4] | (header[3] << 8)); bool fatal = (_data[0] == 2); if (fatal) { throw new TLSRecordException("TLS Alert fatal: " + _data[1].ToString("X2")); } } TLSRecord tlsr = new TLSRecord() { Type = _type, Version = _version, Length = _recordLength, Data = _data }; if (recieveEncrypted) tlsr = Encryption.DecryptRecord(tlsr, tlsCrypt, seq_server); seq_server++; if(tlsr.Type == TLSRecord.TLSR_ChangeSipherSpec) { seq_server = 0; } return tlsr; } public async ValueTask ReadApplicationDataAsync(byte[] dest, int offset, int length) { if (shutdownRequested) return -1; try { int read = 0; if (AvailableApplicationDataBytes > 0) { if (AvailableApplicationDataBytes > length) { //we have more than needed Buffer.BlockCopy(AvailableApplicationData, 0, dest, offset, length); read += length; byte[] remain = new byte[AvailableApplicationDataBytes - length]; Buffer.BlockCopy(AvailableApplicationData, length, remain, 0, remain.Length); AvailableApplicationData = remain; AvailableApplicationDataBytes -= length; return read; } else { //we can exhaust our buffer, but we require more int copylen = AvailableApplicationDataBytes; Buffer.BlockCopy(AvailableApplicationData, 0, dest, offset, copylen); read += copylen; AvailableApplicationData = null; AvailableApplicationDataBytes = 0; offset += copylen; length -= copylen; if (length <= 0) return read; } } TLSRecord next = await ReadNextRecord().ConfigureAwait(false); if (next.Type == TLSRecord.TLSR_Alert) { if(next.Data.Length >= 2) { if (next.Data[1] == 0) { //close notify return read; } } } if (next.Type == TLSRecord.TLSR_ApplicationData) { byte[] newApplicationData = next.Data; if (newApplicationData.Length > length) { Buffer.BlockCopy(newApplicationData, 0, dest, offset, length); byte[] remain = new byte[newApplicationData.Length - length]; Buffer.BlockCopy(newApplicationData, length, remain, 0, remain.Length); AvailableApplicationData = remain; AvailableApplicationDataBytes = remain.Length; read += length; } else { //give them what we can, no need to save any available data Buffer.BlockCopy(newApplicationData, 0, dest, offset, newApplicationData.Length); read += newApplicationData.Length; } return read; } else { return -1; } } catch (Exception ex) { } return -1; } public ValueTask SendApplicationDataAsync(byte[] data) { if (shutdownRequested) return new ValueTask(); int datlen = data.Length; int limit = 16384 - 5 - 20 - 100; limit = preferredFragmentSize; if (datlen > limit) { int bpos = 0; int remain = datlen; while (remain > 0) { int copysize = preferredFragmentSize; if (copysize > remain) copysize = remain; byte[] recdat = new byte[copysize]; Buffer.BlockCopy(data, bpos, recdat, 0, copysize); bpos += copysize; remain -= copysize; TLSRecord fragrec = new TLSRecord(recdat, vers); if (remain <= 0) { //if this is the last in the sequence TaskCompletionSource tcs = new TaskCompletionSource(); ScheduleSend(fragrec, tcs); FlushOutput(); return new ValueTask(tcs.Task); } else { ScheduleSend(fragrec); } } } else { TaskCompletionSource tcs = new TaskCompletionSource(); TLSRecord apld = new TLSRecord(data, vers); ScheduleSend(apld, tcs); FlushOutput(); return new ValueTask(tcs.Task); } return new ValueTask(Task.FromException(new Exception("Failure during dispatch"))); } } }