summary refs log tree commit diff
path: root/Crypto/src/crypto/tls/TlsProtocolHandler.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Crypto/src/crypto/tls/TlsProtocolHandler.cs1259
1 files changed, 1259 insertions, 0 deletions
diff --git a/Crypto/src/crypto/tls/TlsProtocolHandler.cs b/Crypto/src/crypto/tls/TlsProtocolHandler.cs
new file mode 100644

index 000000000..6d2b0b144 --- /dev/null +++ b/Crypto/src/crypto/tls/TlsProtocolHandler.cs
@@ -0,0 +1,1259 @@ +using System; +using System.Collections; +using System.IO; +using System.Text; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Agreement.Srp; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Encodings; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.IO; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Prng; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Date; + +namespace Org.BouncyCastle.Crypto.Tls +{ + /// <remarks>An implementation of all high level protocols in TLS 1.0.</remarks> + public class TlsProtocolHandler + { + /* + * Our Connection states + */ + private const short CS_CLIENT_HELLO_SEND = 1; + private const short CS_SERVER_HELLO_RECEIVED = 2; + private const short CS_SERVER_CERTIFICATE_RECEIVED = 3; + private const short CS_SERVER_KEY_EXCHANGE_RECEIVED = 4; + private const short CS_CERTIFICATE_REQUEST_RECEIVED = 5; + private const short CS_SERVER_HELLO_DONE_RECEIVED = 6; + private const short CS_CLIENT_KEY_EXCHANGE_SEND = 7; + private const short CS_CERTIFICATE_VERIFY_SEND = 8; + private const short CS_CLIENT_CHANGE_CIPHER_SPEC_SEND = 9; + private const short CS_CLIENT_FINISHED_SEND = 10; + private const short CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED = 11; + private const short CS_DONE = 12; + + private static readonly byte[] emptybuf = new byte[0]; + + private static readonly string TLS_ERROR_MESSAGE = "Internal TLS error, this could be an attack"; + + /* + * Queues for data from some protocols. + */ + + private ByteQueue applicationDataQueue = new ByteQueue(); + private ByteQueue changeCipherSpecQueue = new ByteQueue(); + private ByteQueue alertQueue = new ByteQueue(); + private ByteQueue handshakeQueue = new ByteQueue(); + + /* + * The Record Stream we use + */ + private RecordStream rs; + private SecureRandom random; + + private TlsStream tlsStream = null; + + private bool closed = false; + private bool failedWithError = false; + private bool appDataReady = false; + private IDictionary clientExtensions; + + private SecurityParameters securityParameters = null; + + private TlsClientContextImpl tlsClientContext = null; + private TlsClient tlsClient = null; + private CipherSuite[] offeredCipherSuites = null; + private CompressionMethod[] offeredCompressionMethods = null; + private TlsKeyExchange keyExchange = null; + private TlsAuthentication authentication = null; + private CertificateRequest certificateRequest = null; + + private short connection_state = 0; + + private static SecureRandom CreateSecureRandom() + { + /* + * We use our threaded seed generator to generate a good random seed. If the user + * has a better random seed, he should use the constructor with a SecureRandom. + * + * Hopefully, 20 bytes in fast mode are good enough. + */ + byte[] seed = new ThreadedSeedGenerator().GenerateSeed(20, true); + + return new SecureRandom(seed); + } + + public TlsProtocolHandler( + Stream s) + : this(s, s) + { + } + + public TlsProtocolHandler( + Stream s, + SecureRandom sr) + : this(s, s, sr) + { + } + + /// <remarks>Both streams can be the same object</remarks> + public TlsProtocolHandler( + Stream inStr, + Stream outStr) + : this(inStr, outStr, CreateSecureRandom()) + { + } + + /// <remarks>Both streams can be the same object</remarks> + public TlsProtocolHandler( + Stream inStr, + Stream outStr, + SecureRandom sr) + { + this.rs = new RecordStream(this, inStr, outStr); + this.random = sr; + } + + internal void ProcessData( + ContentType protocol, + byte[] buf, + int offset, + int len) + { + /* + * Have a look at the protocol type, and add it to the correct queue. + */ + switch (protocol) + { + case ContentType.change_cipher_spec: + changeCipherSpecQueue.AddData(buf, offset, len); + ProcessChangeCipherSpec(); + break; + case ContentType.alert: + alertQueue.AddData(buf, offset, len); + ProcessAlert(); + break; + case ContentType.handshake: + handshakeQueue.AddData(buf, offset, len); + ProcessHandshake(); + break; + case ContentType.application_data: + if (!appDataReady) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + } + applicationDataQueue.AddData(buf, offset, len); + ProcessApplicationData(); + break; + default: + /* + * Uh, we don't know this protocol. + * + * RFC2246 defines on page 13, that we should ignore this. + */ + break; + } + } + + private void ProcessHandshake() + { + bool read; + do + { + read = false; + + /* + * We need the first 4 bytes, they contain type and length of + * the message. + */ + if (handshakeQueue.Available >= 4) + { + byte[] beginning = new byte[4]; + handshakeQueue.Read(beginning, 0, 4, 0); + MemoryStream bis = new MemoryStream(beginning, false); + HandshakeType type = (HandshakeType)TlsUtilities.ReadUint8(bis); + int len = TlsUtilities.ReadUint24(bis); + + /* + * Check if we have enough bytes in the buffer to read + * the full message. + */ + if (handshakeQueue.Available >= (len + 4)) + { + /* + * Read the message. + */ + byte[] buf = new byte[len]; + handshakeQueue.Read(buf, 0, len, 4); + handshakeQueue.RemoveData(len + 4); + + /* + * RFC 2246 7.4.9. The value handshake_messages includes all + * handshake messages starting at client hello up to, but not + * including, this finished message. [..] Note: [Also,] Hello Request + * messages are omitted from handshake hashes. + */ + switch (type) + { + case HandshakeType.hello_request: + case HandshakeType.finished: + break; + default: + rs.UpdateHandshakeData(beginning, 0, 4); + rs.UpdateHandshakeData(buf, 0, len); + break; + } + + /* + * Now, parse the message. + */ + ProcessHandshakeMessage(type, buf); + read = true; + } + } + } + while (read); + } + + private void ProcessHandshakeMessage(HandshakeType type, byte[] buf) + { + MemoryStream inStr = new MemoryStream(buf, false); + + /* + * Check the type. + */ + switch (type) + { + case HandshakeType.certificate: + { + switch (connection_state) + { + case CS_SERVER_HELLO_RECEIVED: + { + // Parse the Certificate message and send to cipher suite + + Certificate serverCertificate = Certificate.Parse(inStr); + + AssertEmpty(inStr); + + this.keyExchange.ProcessServerCertificate(serverCertificate); + + this.authentication = tlsClient.GetAuthentication(); + this.authentication.NotifyServerCertificate(serverCertificate); + + break; + } + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + + connection_state = CS_SERVER_CERTIFICATE_RECEIVED; + break; + } + case HandshakeType.finished: + switch (connection_state) + { + case CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED: + /* + * Read the checksum from the finished message, it has always 12 bytes. + */ + byte[] serverVerifyData = new byte[12]; + TlsUtilities.ReadFully(serverVerifyData, inStr); + + AssertEmpty(inStr); + + /* + * Calculate our own checksum. + */ + byte[] expectedServerVerifyData = TlsUtilities.PRF( + securityParameters.masterSecret, "server finished", + rs.GetCurrentHash(), 12); + + /* + * Compare both checksums. + */ + if (!Arrays.ConstantTimeAreEqual(expectedServerVerifyData, serverVerifyData)) + { + /* + * Wrong checksum in the finished message. + */ + this.FailWithError(AlertLevel.fatal, AlertDescription.handshake_failure); + } + + connection_state = CS_DONE; + + /* + * We are now ready to receive application data. + */ + this.appDataReady = true; + break; + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + break; + case HandshakeType.server_hello: + switch (connection_state) + { + case CS_CLIENT_HELLO_SEND: + /* + * Read the server hello message + */ + TlsUtilities.CheckVersion(inStr); + + /* + * Read the server random + */ + securityParameters.serverRandom = new byte[32]; + TlsUtilities.ReadFully(securityParameters.serverRandom, inStr); + + byte[] sessionID = TlsUtilities.ReadOpaque8(inStr); + if (sessionID.Length > 32) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.illegal_parameter); + } + + this.tlsClient.NotifySessionID(sessionID); + + /* + * Find out which CipherSuite the server has chosen and check that + * it was one of the offered ones. + */ + CipherSuite selectedCipherSuite = (CipherSuite)TlsUtilities.ReadUint16(inStr); + if (!ArrayContains(offeredCipherSuites, selectedCipherSuite) + || selectedCipherSuite == CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.illegal_parameter); + } + + this.tlsClient.NotifySelectedCipherSuite(selectedCipherSuite); + + /* + * Find out which CompressionMethod the server has chosen and check that + * it was one of the offered ones. + */ + CompressionMethod selectedCompressionMethod = (CompressionMethod)TlsUtilities.ReadUint8(inStr); + if (!ArrayContains(offeredCompressionMethods, selectedCompressionMethod)) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.illegal_parameter); + } + + this.tlsClient.NotifySelectedCompressionMethod(selectedCompressionMethod); + + /* + * RFC3546 2.2 The extended server hello message format MAY be + * sent in place of the server hello message when the client has + * requested extended functionality via the extended client hello + * message specified in Section 2.1. + * ... + * Note that the extended server hello message is only sent in response + * to an extended client hello message. This prevents the possibility + * that the extended server hello message could "break" existing TLS 1.0 + * clients. + */ + + /* + * TODO RFC 3546 2.3 + * If [...] the older session is resumed, then the server MUST ignore + * extensions appearing in the client hello, and send a server hello + * containing no extensions. + */ + + // ExtensionType -> byte[] + IDictionary serverExtensions = Platform.CreateHashtable(); + + if (inStr.Position < inStr.Length) + { + // Process extensions from extended server hello + byte[] extBytes = TlsUtilities.ReadOpaque16(inStr); + + MemoryStream ext = new MemoryStream(extBytes, false); + while (ext.Position < ext.Length) + { + ExtensionType extType = (ExtensionType)TlsUtilities.ReadUint16(ext); + byte[] extValue = TlsUtilities.ReadOpaque16(ext); + + // Note: RFC 5746 makes a special case for EXT_RenegotiationInfo + if (extType != ExtensionType.renegotiation_info + && !clientExtensions.Contains(extType)) + { + /* + * RFC 3546 2.3 + * Note that for all extension types (including those defined in + * future), the extension type MUST NOT appear in the extended server + * hello unless the same extension type appeared in the corresponding + * client hello. Thus clients MUST abort the handshake if they receive + * an extension type in the extended server hello that they did not + * request in the associated (extended) client hello. + */ + this.FailWithError(AlertLevel.fatal, AlertDescription.unsupported_extension); + } + + if (serverExtensions.Contains(extType)) + { + /* + * RFC 3546 2.3 + * Also note that when multiple extensions of different types are + * present in the extended client hello or the extended server hello, + * the extensions may appear in any order. There MUST NOT be more than + * one extension of the same type. + */ + this.FailWithError(AlertLevel.fatal, AlertDescription.illegal_parameter); + } + + serverExtensions.Add(extType, extValue); + } + } + + AssertEmpty(inStr); + + /* + * RFC 5746 3.4. When a ServerHello is received, the client MUST check if it + * includes the "renegotiation_info" extension: + */ + { + bool secure_negotiation = serverExtensions.Contains(ExtensionType.renegotiation_info); + + /* + * If the extension is present, set the secure_renegotiation flag + * to TRUE. The client MUST then verify that the length of the + * "renegotiated_connection" field is zero, and if it is not, MUST + * abort the handshake (by sending a fatal handshake_failure + * alert). + */ + if (secure_negotiation) + { + byte[] renegExtValue = (byte[])serverExtensions[ExtensionType.renegotiation_info]; + + if (!Arrays.ConstantTimeAreEqual(renegExtValue, + CreateRenegotiationInfo(emptybuf))) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.handshake_failure); + } + } + + tlsClient.NotifySecureRenegotiation(secure_negotiation); + } + + if (clientExtensions != null) + { + tlsClient.ProcessServerExtensions(serverExtensions); + } + + this.keyExchange = tlsClient.GetKeyExchange(); + + connection_state = CS_SERVER_HELLO_RECEIVED; + break; + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + break; + case HandshakeType.server_hello_done: + switch (connection_state) + { + case CS_SERVER_HELLO_RECEIVED: + case CS_SERVER_CERTIFICATE_RECEIVED: + case CS_SERVER_KEY_EXCHANGE_RECEIVED: + case CS_CERTIFICATE_REQUEST_RECEIVED: + + // NB: Original code used case label fall-through + + if (connection_state == CS_SERVER_HELLO_RECEIVED) + { + // There was no server certificate message; check it's OK + this.keyExchange.SkipServerCertificate(); + this.authentication = null; + + // There was no server key exchange message; check it's OK + this.keyExchange.SkipServerKeyExchange(); + } + else if (connection_state == CS_SERVER_CERTIFICATE_RECEIVED) + { + // There was no server key exchange message; check it's OK + this.keyExchange.SkipServerKeyExchange(); + } + + AssertEmpty(inStr); + + connection_state = CS_SERVER_HELLO_DONE_RECEIVED; + + TlsCredentials clientCreds = null; + if (certificateRequest == null) + { + this.keyExchange.SkipClientCredentials(); + } + else + { + clientCreds = this.authentication.GetClientCredentials(certificateRequest); + + Certificate clientCert; + if (clientCreds == null) + { + this.keyExchange.SkipClientCredentials(); + clientCert = Certificate.EmptyChain; + } + else + { + this.keyExchange.ProcessClientCredentials(clientCreds); + clientCert = clientCreds.Certificate; + } + + SendClientCertificate(clientCert); + } + + /* + * Send the client key exchange message, depending on the key + * exchange we are using in our CipherSuite. + */ + SendClientKeyExchange(); + + connection_state = CS_CLIENT_KEY_EXCHANGE_SEND; + + if (clientCreds != null && clientCreds is TlsSignerCredentials) + { + TlsSignerCredentials signerCreds = (TlsSignerCredentials)clientCreds; + byte[] md5andsha1 = rs.GetCurrentHash(); + byte[] clientCertificateSignature = signerCreds.GenerateCertificateSignature( + md5andsha1); + SendCertificateVerify(clientCertificateSignature); + + connection_state = CS_CERTIFICATE_VERIFY_SEND; + } + + /* + * Now, we send change cipher state + */ + byte[] cmessage = new byte[1]; + cmessage[0] = 1; + rs.WriteMessage(ContentType.change_cipher_spec, cmessage, 0, cmessage.Length); + + connection_state = CS_CLIENT_CHANGE_CIPHER_SPEC_SEND; + + /* + * Calculate the master_secret + */ + byte[] pms = this.keyExchange.GeneratePremasterSecret(); + + securityParameters.masterSecret = TlsUtilities.PRF(pms, "master secret", + TlsUtilities.Concat(securityParameters.clientRandom, securityParameters.serverRandom), + 48); + + // TODO Is there a way to ensure the data is really overwritten? + /* + * RFC 2246 8.1. The pre_master_secret should be deleted from + * memory once the master_secret has been computed. + */ + Array.Clear(pms, 0, pms.Length); + + /* + * Initialize our cipher suite + */ + rs.ClientCipherSpecDecided(tlsClient.GetCompression(), tlsClient.GetCipher()); + + /* + * Send our finished message. + */ + byte[] clientVerifyData = TlsUtilities.PRF(securityParameters.masterSecret, + "client finished", rs.GetCurrentHash(), 12); + + MemoryStream bos = new MemoryStream(); + TlsUtilities.WriteUint8((byte)HandshakeType.finished, bos); + TlsUtilities.WriteOpaque24(clientVerifyData, bos); + byte[] message = bos.ToArray(); + + rs.WriteMessage(ContentType.handshake, message, 0, message.Length); + + this.connection_state = CS_CLIENT_FINISHED_SEND; + break; + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.handshake_failure); + break; + } + break; + case HandshakeType.server_key_exchange: + { + switch (connection_state) + { + case CS_SERVER_HELLO_RECEIVED: + case CS_SERVER_CERTIFICATE_RECEIVED: + { + // NB: Original code used case label fall-through + if (connection_state == CS_SERVER_HELLO_RECEIVED) + { + // There was no server certificate message; check it's OK + this.keyExchange.SkipServerCertificate(); + this.authentication = null; + } + + this.keyExchange.ProcessServerKeyExchange(inStr); + + AssertEmpty(inStr); + break; + } + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + + this.connection_state = CS_SERVER_KEY_EXCHANGE_RECEIVED; + break; + } + case HandshakeType.certificate_request: + switch (connection_state) + { + case CS_SERVER_CERTIFICATE_RECEIVED: + case CS_SERVER_KEY_EXCHANGE_RECEIVED: + { + // NB: Original code used case label fall-through + if (connection_state == CS_SERVER_CERTIFICATE_RECEIVED) + { + // There was no server key exchange message; check it's OK + this.keyExchange.SkipServerKeyExchange(); + } + + if (this.authentication == null) + { + /* + * RFC 2246 7.4.4. It is a fatal handshake_failure alert + * for an anonymous server to request client identification. + */ + this.FailWithError(AlertLevel.fatal, AlertDescription.handshake_failure); + } + + int numTypes = TlsUtilities.ReadUint8(inStr); + ClientCertificateType[] certificateTypes = new ClientCertificateType[numTypes]; + for (int i = 0; i < numTypes; ++i) + { + certificateTypes[i] = (ClientCertificateType)TlsUtilities.ReadUint8(inStr); + } + + byte[] authorities = TlsUtilities.ReadOpaque16(inStr); + + AssertEmpty(inStr); + + IList authorityDNs = Platform.CreateArrayList(); + + MemoryStream bis = new MemoryStream(authorities, false); + while (bis.Position < bis.Length) + { + byte[] dnBytes = TlsUtilities.ReadOpaque16(bis); + // TODO Switch to X500Name when available + authorityDNs.Add(X509Name.GetInstance(Asn1Object.FromByteArray(dnBytes))); + } + + this.certificateRequest = new CertificateRequest(certificateTypes, + authorityDNs); + this.keyExchange.ValidateCertificateRequest(this.certificateRequest); + + break; + } + default: + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + + this.connection_state = CS_CERTIFICATE_REQUEST_RECEIVED; + break; + case HandshakeType.hello_request: + /* + * RFC 2246 7.4.1.1 Hello request + * This message will be ignored by the client if the client is currently + * negotiating a session. This message may be ignored by the client if it + * does not wish to renegotiate a session, or the client may, if it wishes, + * respond with a no_renegotiation alert. + */ + if (connection_state == CS_DONE) + { + // Renegotiation not supported yet + SendAlert(AlertLevel.warning, AlertDescription.no_renegotiation); + } + break; + case HandshakeType.client_key_exchange: + case HandshakeType.certificate_verify: + case HandshakeType.client_hello: + default: + // We do not support this! + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + break; + } + } + + private void ProcessApplicationData() + { + /* + * There is nothing we need to do here. + * + * This function could be used for callbacks when application + * data arrives in the future. + */ + } + + private void ProcessAlert() + { + while (alertQueue.Available >= 2) + { + /* + * An alert is always 2 bytes. Read the alert. + */ + byte[] tmp = new byte[2]; + alertQueue.Read(tmp, 0, 2, 0); + alertQueue.RemoveData(2); + byte level = tmp[0]; + byte description = tmp[1]; + if (level == (byte)AlertLevel.fatal) + { + /* + * This is a fatal error. + */ + this.failedWithError = true; + this.closed = true; + /* + * Now try to Close the stream, ignore errors. + */ + try + { + rs.Close(); + } + catch (Exception) + { + } + throw new IOException(TLS_ERROR_MESSAGE); + } + else + { + /* + * This is just a warning. + */ + if (description == (byte)AlertDescription.close_notify) + { + /* + * Close notify + */ + this.FailWithError(AlertLevel.warning, AlertDescription.close_notify); + } + /* + * If it is just a warning, we continue. + */ + } + } + } + + /** + * This method is called, when a change cipher spec message is received. + * + * @throws IOException If the message has an invalid content or the + * handshake is not in the correct state. + */ + private void ProcessChangeCipherSpec() + { + while (changeCipherSpecQueue.Available > 0) + { + /* + * A change cipher spec message is only one byte with the value 1. + */ + byte[] b = new byte[1]; + changeCipherSpecQueue.Read(b, 0, 1, 0); + changeCipherSpecQueue.RemoveData(1); + if (b[0] != 1) + { + /* + * This should never happen. + */ + this.FailWithError(AlertLevel.fatal, AlertDescription.unexpected_message); + } + + /* + * Check if we are in the correct connection state. + */ + if (this.connection_state != CS_CLIENT_FINISHED_SEND) + { + this.FailWithError(AlertLevel.fatal, AlertDescription.handshake_failure); + } + + rs.ServerClientSpecReceived(); + + this.connection_state = CS_SERVER_CHANGE_CIPHER_SPEC_RECEIVED; + } + } + + private void SendClientCertificate(Certificate clientCert) + { + MemoryStream bos = new MemoryStream(); + TlsUtilities.WriteUint8((byte)HandshakeType.certificate, bos); + + // Reserve space for length + TlsUtilities.WriteUint24(0, bos); + + clientCert.Encode(bos); + byte[] message = bos.ToArray(); + + // Patch actual length back in + TlsUtilities.WriteUint24(message.Length - 4, message, 1); + + rs.WriteMessage(ContentType.handshake, message, 0, message.Length); + } + + private void SendClientKeyExchange() + { + MemoryStream bos = new MemoryStream(); + TlsUtilities.WriteUint8((byte)HandshakeType.client_key_exchange, bos); + + // Reserve space for length + TlsUtilities.WriteUint24(0, bos); + + this.keyExchange.GenerateClientKeyExchange(bos); + byte[] message = bos.ToArray(); + + // Patch actual length back in + TlsUtilities.WriteUint24(message.Length - 4, message, 1); + + rs.WriteMessage(ContentType.handshake, message, 0, message.Length); + } + + private void SendCertificateVerify(byte[] data) + { + /* + * Send signature of handshake messages so far to prove we are the owner of + * the cert See RFC 2246 sections 4.7, 7.4.3 and 7.4.8 + */ + MemoryStream bos = new MemoryStream(); + TlsUtilities.WriteUint8((byte)HandshakeType.certificate_verify, bos); + TlsUtilities.WriteUint24(data.Length + 2, bos); + TlsUtilities.WriteOpaque16(data, bos); + byte[] message = bos.ToArray(); + + rs.WriteMessage(ContentType.handshake, message, 0, message.Length); + } + + /// <summary>Connects to the remote system.</summary> + /// <param name="verifyer">Will be used when a certificate is received to verify + /// that this certificate is accepted by the client.</param> + /// <exception cref="IOException">If handshake was not successful</exception> + [Obsolete("Use version taking TlsClient")] + public virtual void Connect( + ICertificateVerifyer verifyer) + { + this.Connect(new LegacyTlsClient(verifyer)); + } + + public virtual void Connect(TlsClient tlsClient) + { + if (tlsClient == null) + throw new ArgumentNullException("tlsClient"); + if (this.tlsClient != null) + throw new InvalidOperationException("Connect can only be called once"); + + /* + * Send Client hello + * + * First, generate some random data. + */ + this.securityParameters = new SecurityParameters(); + this.securityParameters.clientRandom = new byte[32]; + random.NextBytes(securityParameters.clientRandom, 4, 28); + TlsUtilities.WriteGmtUnixTime(securityParameters.clientRandom, 0); + + this.tlsClientContext = new TlsClientContextImpl(random, securityParameters); + this.tlsClient = tlsClient; + this.tlsClient.Init(tlsClientContext); + + MemoryStream outStr = new MemoryStream(); + TlsUtilities.WriteVersion(outStr); + outStr.Write(securityParameters.clientRandom, 0, 32); + + /* + * Length of Session id + */ + TlsUtilities.WriteUint8(0, outStr); + + this.offeredCipherSuites = this.tlsClient.GetCipherSuites(); + + // ExtensionType -> byte[] + this.clientExtensions = this.tlsClient.GetClientExtensions(); + + // Cipher Suites (and SCSV) + { + /* + * RFC 5746 3.4. + * The client MUST include either an empty "renegotiation_info" + * extension, or the TLS_EMPTY_RENEGOTIATION_INFO_SCSV signaling + * cipher suite value in the ClientHello. Including both is NOT + * RECOMMENDED. + */ + bool noRenegExt = clientExtensions == null + || !clientExtensions.Contains(ExtensionType.renegotiation_info); + + int count = offeredCipherSuites.Length; + if (noRenegExt) + { + // Note: 1 extra slot for TLS_EMPTY_RENEGOTIATION_INFO_SCSV + ++count; + } + + TlsUtilities.WriteUint16(2 * count, outStr); + + for (int i = 0; i < offeredCipherSuites.Length; ++i) + { + TlsUtilities.WriteUint16((int)offeredCipherSuites[i], outStr); + } + + if (noRenegExt) + { + TlsUtilities.WriteUint16((int)CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV, outStr); + } + } + + /* + * Compression methods, just the null method. + */ + this.offeredCompressionMethods = tlsClient.GetCompressionMethods(); + + { + TlsUtilities.WriteUint8((byte)offeredCompressionMethods.Length, outStr); + for (int i = 0; i < offeredCompressionMethods.Length; ++i) + { + TlsUtilities.WriteUint8((byte)offeredCompressionMethods[i], outStr); + } + } + + // Extensions + if (clientExtensions != null) + { + MemoryStream ext = new MemoryStream(); + + foreach (ExtensionType extType in clientExtensions.Keys) + { + WriteExtension(ext, extType, (byte[])clientExtensions[extType]); + } + + TlsUtilities.WriteOpaque16(ext.ToArray(), outStr); + } + + MemoryStream bos = new MemoryStream(); + TlsUtilities.WriteUint8((byte)HandshakeType.client_hello, bos); + TlsUtilities.WriteUint24((int)outStr.Length, bos); + byte[] outBytes = outStr.ToArray(); + bos.Write(outBytes, 0, outBytes.Length); + byte[] message = bos.ToArray(); + SafeWriteMessage(ContentType.handshake, message, 0, message.Length); + connection_state = CS_CLIENT_HELLO_SEND; + + /* + * We will now read data, until we have completed the handshake. + */ + while (connection_state != CS_DONE) + { + SafeReadData(); + } + + this.tlsStream = new TlsStream(this); + } + + /** + * Read data from the network. The method will return immediately, if there is + * still some data left in the buffer, or block until some application + * data has been read from the network. + * + * @param buf The buffer where the data will be copied to. + * @param offset The position where the data will be placed in the buffer. + * @param len The maximum number of bytes to read. + * @return The number of bytes read. + * @throws IOException If something goes wrong during reading data. + */ + internal int ReadApplicationData(byte[] buf, int offset, int len) + { + while (applicationDataQueue.Available == 0) + { + if (this.closed) + { + /* + * We need to read some data. + */ + if (this.failedWithError) + { + /* + * Something went terribly wrong, we should throw an IOException + */ + throw new IOException(TLS_ERROR_MESSAGE); + } + + /* + * Connection has been closed, there is no more data to read. + */ + return 0; + } + + SafeReadData(); + } + len = System.Math.Min(len, applicationDataQueue.Available); + applicationDataQueue.Read(buf, offset, len, 0); + applicationDataQueue.RemoveData(len); + return len; + } + + private void SafeReadData() + { + try + { + rs.ReadData(); + } + catch (TlsFatalAlert e) + { + if (!this.closed) + { + this.FailWithError(e.AlertDescription, e); + } + throw e; + } + catch (IOException e) + { + if (!this.closed) + { + this.FailWithError(AlertDescription.internal_error, e); + } + throw e; + } + catch (Exception e) + { + if (!this.closed) + { + this.FailWithError(AlertDescription.internal_error, e); + } + throw e; + } + } + + private void SafeWriteMessage(ContentType type, byte[] buf, int offset, int len) + { + try + { + rs.WriteMessage(type, buf, offset, len); + } + catch (TlsFatalAlert e) + { + if (!this.closed) + { + this.FailWithError(e.AlertDescription, e); + } + throw e; + } + catch (IOException e) + { + if (!closed) + { + this.FailWithError(AlertDescription.internal_error, e); + } + throw e; + } + catch (Exception e) + { + if (!closed) + { + this.FailWithError(AlertDescription.internal_error, e); + } + throw e; + } + } + + /** + * Send some application data to the remote system. + * <p/> + * The method will handle fragmentation internally. + * + * @param buf The buffer with the data. + * @param offset The position in the buffer where the data is placed. + * @param len The length of the data. + * @throws IOException If something goes wrong during sending. + */ + internal void WriteData(byte[] buf, int offset, int len) + { + if (this.closed) + { + if (this.failedWithError) + throw new IOException(TLS_ERROR_MESSAGE); + + throw new IOException("Sorry, connection has been closed, you cannot write more data"); + } + + /* + * Protect against known IV attack! + * + * DO NOT REMOVE THIS LINE, EXCEPT YOU KNOW EXACTLY WHAT + * YOU ARE DOING HERE. + */ + SafeWriteMessage(ContentType.application_data, emptybuf, 0, 0); + + do + { + /* + * We are only allowed to write fragments up to 2^14 bytes. + */ + int toWrite = System.Math.Min(len, 1 << 14); + + SafeWriteMessage(ContentType.application_data, buf, offset, toWrite); + + offset += toWrite; + len -= toWrite; + } + while (len > 0); + } + + /// <summary>A Stream which can be used to send data.</summary> + [Obsolete("Use 'Stream' property instead")] + public virtual Stream OutputStream + { + get { return this.tlsStream; } + } + + /// <summary>A Stream which can be used to read data.</summary> + [Obsolete("Use 'Stream' property instead")] + public virtual Stream InputStream + { + get { return this.tlsStream; } + } + + /// <summary>The secure bidirectional stream for this connection</summary> + public virtual Stream Stream + { + get { return this.tlsStream; } + } + + /** + * Terminate this connection with an alert. + * <p/> + * Can be used for normal closure too. + * + * @param alertLevel The level of the alert, an be AlertLevel.fatal or AL_warning. + * @param alertDescription The exact alert message. + * @throws IOException If alert was fatal. + */ + private void FailWithError(AlertLevel alertLevel, AlertDescription alertDescription) + { + this.FailWithError(alertLevel, alertDescription, null); + } + + private void FailWithError(AlertDescription alertDescription, Exception ex) + { + this.FailWithError(AlertLevel.fatal, alertDescription, ex); + } + + private void FailWithError(AlertLevel alertLevel, AlertDescription alertDescription, Exception ex) + { + /* + * Check if the connection is still open. + */ + if (!closed) + { + /* + * Prepare the message + */ + this.closed = true; + + if (alertLevel == AlertLevel.fatal) + { + /* + * This is a fatal message. + */ + this.failedWithError = true; + } + SendAlert(alertLevel, alertDescription); + rs.Close(); + if (alertLevel == AlertLevel.fatal) + { + throw new IOException(TLS_ERROR_MESSAGE, ex); + } + } + else + { + throw new IOException(TLS_ERROR_MESSAGE, ex); + } + } + + internal void SendAlert(AlertLevel alertLevel, AlertDescription alertDescription) + { + byte[] error = new byte[2]; + error[0] = (byte)alertLevel; + error[1] = (byte)alertDescription; + + rs.WriteMessage(ContentType.alert, error, 0, 2); + } + + /// <summary>Closes this connection</summary> + /// <exception cref="IOException">If something goes wrong during closing.</exception> + public virtual void Close() + { + if (!closed) + { + this.FailWithError(AlertLevel.warning, AlertDescription.close_notify); + } + } + + /** + * Make sure the Stream is now empty. Fail otherwise. + * + * @param is The Stream to check. + * @throws IOException If is is not empty. + */ + internal void AssertEmpty( + MemoryStream inStr) + { + if (inStr.Position < inStr.Length) + { + throw new TlsFatalAlert(AlertDescription.decode_error); + } + } + + internal void Flush() + { + rs.Flush(); + } + + internal bool IsClosed + { + get { return closed; } + } + + private static bool ArrayContains(CipherSuite[] a, CipherSuite n) + { + for (int i = 0; i < a.Length; ++i) + { + if (a[i] == n) + return true; + } + return false; + } + + private static bool ArrayContains(CompressionMethod[] a, CompressionMethod n) + { + for (int i = 0; i < a.Length; ++i) + { + if (a[i] == n) + return true; + } + return false; + } + + private static byte[] CreateRenegotiationInfo(byte[] renegotiated_connection) + { + MemoryStream buf = new MemoryStream(); + TlsUtilities.WriteOpaque8(renegotiated_connection, buf); + return buf.ToArray(); + } + + private static void WriteExtension(Stream output, ExtensionType extType, byte[] extValue) + { + TlsUtilities.WriteUint16((int)extType, output); + TlsUtilities.WriteOpaque16(extValue, output); + } + } +}