Fig. 1 - Message flow for a full handshake
*/
- internal override IAsyncResult OnBeginNegotiateHandshake(AsyncCallback callback, object state)
+ private void SafeEndReceiveRecord (IAsyncResult ar, bool ignoreEmpty = false)
{
- try
+ byte[] record = this.protocol.EndReceiveRecord (ar);
+ if (!ignoreEmpty && ((record == null) || (record.Length == 0))) {
+ throw new TlsException (
+ AlertDescription.HandshakeFailiure,
+ "The server stopped the handshake.");
+ }
+ }
+
+ private enum NegotiateState
+ {
+ SentClientHello,
+ ReceiveClientHelloResponse,
+ SentCipherSpec,
+ ReceiveCipherSpecResponse,
+ SentKeyExchange,
+ ReceiveFinishResponse,
+ SentFinished,
+ };
+
+ private class NegotiateAsyncResult : IAsyncResult
+ {
+ private object locker = new object ();
+ private AsyncCallback _userCallback;
+ private object _userState;
+ private Exception _asyncException;
+ private ManualResetEvent handle;
+ private NegotiateState _state;
+ private bool completed;
+
+ public NegotiateAsyncResult(AsyncCallback userCallback, object userState, NegotiateState state)
{
- if (this.context.HandshakeState != HandshakeState.None)
- {
- this.context.Clear();
- }
+ _userCallback = userCallback;
+ _userState = userState;
+ _state = state;
+ }
- // Obtain supported cipher suites
- this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers(this.context.SecurityProtocol);
+ public NegotiateState State
+ {
+ get { return _state; }
+ set { _state = value; }
+ }
- // Set handshake state
- this.context.HandshakeState = HandshakeState.Started;
+ public object AsyncState
+ {
+ get { return _userState; }
+ }
- // Send client hello
- return this.protocol.BeginSendRecord(HandshakeType.ClientHello, callback, state);
+ public Exception AsyncException
+ {
+ get { return _asyncException; }
}
- catch (TlsException ex)
+
+ public bool CompletedWithError
{
- this.protocol.SendAlert(ex.Alert);
+ get {
+ if (!IsCompleted)
+ return false; // Perhaps throw InvalidOperationExcetion?
- throw new IOException("The authentication or decryption has failed.", ex);
+ return null != _asyncException;
+ }
}
- catch (Exception ex)
+
+ public WaitHandle AsyncWaitHandle
{
- this.protocol.SendAlert(AlertDescription.InternalError);
+ get {
+ lock (locker) {
+ if (handle == null)
+ handle = new ManualResetEvent (completed);
+ }
+ return handle;
+ }
+
+ }
+
+ public bool CompletedSynchronously
+ {
+ get { return false; }
+ }
+
+ public bool IsCompleted
+ {
+ get {
+ lock (locker) {
+ return completed;
+ }
+ }
+ }
+
+ public void SetComplete(Exception ex)
+ {
+ lock (locker) {
+ if (completed)
+ return;
- throw new IOException("The authentication or decryption has failed.", ex);
+ completed = true;
+ if (handle != null)
+ handle.Set ();
+
+ if (_userCallback != null)
+ _userCallback.BeginInvoke (this, null, null);
+
+ _asyncException = ex;
+ }
+ }
+
+ public void SetComplete()
+ {
+ SetComplete(null);
}
}
- private void SafeReceiveRecord (Stream s, bool ignoreEmpty = false)
+ internal override IAsyncResult BeginNegotiateHandshake(AsyncCallback callback, object state)
{
- byte[] record = this.protocol.ReceiveRecord (s);
- if (!ignoreEmpty && ((record == null) || (record.Length == 0))) {
- throw new TlsException (
- AlertDescription.HandshakeFailiure,
- "The server stopped the handshake.");
+ if (this.context.HandshakeState != HandshakeState.None) {
+ this.context.Clear ();
}
+
+ // Obtain supported cipher suites
+ this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers (this.context.SecurityProtocol);
+
+ // Set handshake state
+ this.context.HandshakeState = HandshakeState.Started;
+
+ NegotiateAsyncResult result = new NegotiateAsyncResult (callback, state, NegotiateState.SentClientHello);
+
+ // Begin sending the client hello
+ this.protocol.BeginSendRecord (HandshakeType.ClientHello, NegotiateAsyncWorker, result);
+
+ return result;
+ }
+
+ internal override void EndNegotiateHandshake (IAsyncResult result)
+ {
+ NegotiateAsyncResult negotiate = result as NegotiateAsyncResult;
+
+ if (negotiate == null)
+ throw new ArgumentNullException ();
+ if (!negotiate.IsCompleted)
+ negotiate.AsyncWaitHandle.WaitOne();
+ if (negotiate.CompletedWithError)
+ throw negotiate.AsyncException;
}
- internal override void OnNegotiateHandshakeCallback(IAsyncResult asyncResult)
+ private void NegotiateAsyncWorker (IAsyncResult result)
{
- this.protocol.EndSendRecord(asyncResult);
+ NegotiateAsyncResult negotiate = result.AsyncState as NegotiateAsyncResult;
- // Read server response
- while (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone)
+ try
{
- // Read next record (skip empty, e.g. warnings alerts)
- SafeReceiveRecord (this.innerStream, true);
+ switch (negotiate.State)
+ {
+ case NegotiateState.SentClientHello:
+ this.protocol.EndSendRecord (result);
- // special case for abbreviated handshake where no ServerHelloDone is sent from the server
- if (this.context.AbbreviatedHandshake && (this.context.LastHandshakeMsg == HandshakeType.ServerHello))
+ // we are now ready to ready the receive the hello response.
+ negotiate.State = NegotiateState.ReceiveClientHelloResponse;
+
+ // Start reading the client hello response
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
break;
- }
- // the handshake is much easier if we can reuse a previous session settings
- if (this.context.AbbreviatedHandshake)
- {
- ClientSessionCache.SetContextFromCache (this.context);
- this.context.Negotiating.Cipher.ComputeKeys ();
- this.context.Negotiating.Cipher.InitializeCipher ();
+ case NegotiateState.ReceiveClientHelloResponse:
+ this.SafeEndReceiveRecord (result, true);
+
+ if (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone &&
+ (!this.context.AbbreviatedHandshake || this.context.LastHandshakeMsg != HandshakeType.ServerHello)) {
+ // Read next record (skip empty, e.g. warnings alerts)
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
+ break;
+ }
+
+ // special case for abbreviated handshake where no ServerHelloDone is sent from the server
+ if (this.context.AbbreviatedHandshake) {
+ ClientSessionCache.SetContextFromCache (this.context);
+ this.context.Negotiating.Cipher.ComputeKeys ();
+ this.context.Negotiating.Cipher.InitializeCipher ();
+
+ negotiate.State = NegotiateState.SentCipherSpec;
+
+ // Send Change Cipher Spec message with the current cipher
+ // or as plain text if this is the initial negotiation
+ this.protocol.BeginSendChangeCipherSpec(NegotiateAsyncWorker, negotiate);
+ } else {
+ // Send client certificate if requested
+ // even if the server ask for it it _may_ still be optional
+ bool clientCertificate = this.context.ServerSettings.CertificateRequest;
+
+ using (var memstream = new MemoryStream())
+ {
+ // NOTE: sadly SSL3 and TLS1 differs in how they handle this and
+ // the current design doesn't allow a very cute way to handle
+ // SSL3 alert warning for NoCertificate (41).
+ if (this.context.SecurityProtocol == SecurityProtocolType.Ssl3)
+ {
+ clientCertificate = ((this.context.ClientSettings.Certificates != null) &&
+ (this.context.ClientSettings.Certificates.Count > 0));
+ // this works well with OpenSSL (but only for SSL3)
+ }
+
+ byte[] record = null;
+
+ if (clientCertificate)
+ {
+ record = this.protocol.EncodeHandshakeRecord(HandshakeType.Certificate);
+ memstream.Write(record, 0, record.Length);
+ }
+
+ // Send Client Key Exchange
+ record = this.protocol.EncodeHandshakeRecord(HandshakeType.ClientKeyExchange);
+ memstream.Write(record, 0, record.Length);
+
+ // Now initialize session cipher with the generated keys
+ this.context.Negotiating.Cipher.InitializeCipher();
+
+ // Send certificate verify if requested (optional)
+ if (clientCertificate && (this.context.ClientSettings.ClientCertificate != null))
+ {
+ record = this.protocol.EncodeHandshakeRecord(HandshakeType.CertificateVerify);
+ memstream.Write(record, 0, record.Length);
+ }
+
+ // send the chnage cipher spec.
+ this.protocol.SendChangeCipherSpec(memstream);
+
+ // Send Finished message
+ record = this.protocol.EncodeHandshakeRecord(HandshakeType.Finished);
+ memstream.Write(record, 0, record.Length);
+
+ negotiate.State = NegotiateState.SentKeyExchange;
+
+ // send all the records.
+ this.innerStream.BeginWrite (memstream.GetBuffer (), 0, (int)memstream.Length, NegotiateAsyncWorker, negotiate);
+ }
+ }
+ break;
- // Send Cipher Spec protocol
- this.protocol.SendChangeCipherSpec ();
+ case NegotiateState.SentKeyExchange:
+ this.innerStream.EndWrite (result);
- // Read record until server finished is received
- while (this.context.HandshakeState != HandshakeState.Finished)
- {
- // If all goes well this will process messages:
- // Change Cipher Spec
- // Server finished
- SafeReceiveRecord (this.innerStream);
- }
+ negotiate.State = NegotiateState.ReceiveFinishResponse;
- // Send Finished message
- this.protocol.SendRecord (HandshakeType.Finished);
- }
- else
- {
- // Send client certificate if requested
- // even if the server ask for it it _may_ still be optional
- bool clientCertificate = this.context.ServerSettings.CertificateRequest;
-
- // NOTE: sadly SSL3 and TLS1 differs in how they handle this and
- // the current design doesn't allow a very cute way to handle
- // SSL3 alert warning for NoCertificate (41).
- if (this.context.SecurityProtocol == SecurityProtocolType.Ssl3)
- {
- clientCertificate = ((this.context.ClientSettings.Certificates != null) &&
- (this.context.ClientSettings.Certificates.Count > 0));
- // this works well with OpenSSL (but only for SSL3)
- }
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
- if (clientCertificate)
- {
- this.protocol.SendRecord(HandshakeType.Certificate);
- }
+ break;
- // Send Client Key Exchange
- this.protocol.SendRecord(HandshakeType.ClientKeyExchange);
+ case NegotiateState.ReceiveFinishResponse:
+ this.SafeEndReceiveRecord (result);
+
+ // Read record until server finished is received
+ if (this.context.HandshakeState != HandshakeState.Finished) {
+ // If all goes well this will process messages:
+ // Change Cipher Spec
+ // Server finished
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
+ }
+ else {
+ // Reset Handshake messages information
+ this.context.HandshakeMessages.Reset ();
+
+ // Clear Key Info
+ this.context.ClearKeyInfo();
+
+ negotiate.SetComplete ();
+ }
+ break;
- // Now initialize session cipher with the generated keys
- this.context.Negotiating.Cipher.InitializeCipher();
- // Send certificate verify if requested (optional)
- if (clientCertificate && (this.context.ClientSettings.ClientCertificate != null))
- {
- this.protocol.SendRecord(HandshakeType.CertificateVerify);
- }
+ case NegotiateState.SentCipherSpec:
+ this.protocol.EndSendChangeCipherSpec (result);
- // Send Cipher Spec protocol
- this.protocol.SendChangeCipherSpec ();
+ negotiate.State = NegotiateState.ReceiveCipherSpecResponse;
- // Send Finished message
- this.protocol.SendRecord (HandshakeType.Finished);
+ // Start reading the cipher spec response
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
+ break;
- // Read record until server finished is received
- while (this.context.HandshakeState != HandshakeState.Finished) {
- // If all goes well this will process messages:
- // Change Cipher Spec
- // Server finished
- SafeReceiveRecord (this.innerStream);
- }
- }
+ case NegotiateState.ReceiveCipherSpecResponse:
+ this.SafeEndReceiveRecord (result, true);
+
+ if (this.context.HandshakeState != HandshakeState.Finished)
+ {
+ this.protocol.BeginReceiveRecord (this.innerStream, NegotiateAsyncWorker, negotiate);
+ }
+ else
+ {
+ negotiate.State = NegotiateState.SentFinished;
+ this.protocol.BeginSendRecord(HandshakeType.Finished, NegotiateAsyncWorker, negotiate);
+ }
+ break;
+
+ case NegotiateState.SentFinished:
+ this.protocol.EndSendRecord (result);
+
+ // Reset Handshake messages information
+ this.context.HandshakeMessages.Reset ();
- // Reset Handshake messages information
- this.context.HandshakeMessages.Reset ();
+ // Clear Key Info
+ this.context.ClearKeyInfo();
- // Clear Key Info
- this.context.ClearKeyInfo();
+ negotiate.SetComplete ();
+ break;
+ }
+ }
+ catch (TlsException ex)
+ {
+ // FIXME: should the send alert also be done asynchronously here and below?
+ this.protocol.SendAlert(ex.Alert);
+ negotiate.SetComplete (new IOException("The authentication or decryption has failed.", ex));
+ }
+ catch (Exception ex)
+ {
+ this.protocol.SendAlert(AlertDescription.InternalError);
+ negotiate.SetComplete (new IOException("The authentication or decryption has failed.", ex));
+ }
}
#endregion