-/* Transport Security Layer (TLS)
- * Copyright (c) 2003-2004 Carlos Guzman Alvarez
- *
- * Permission is hereby granted, free of charge, to any person
- * obtaining a copy of this software and associated documentation
- * files (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge,
- * publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included
- * in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
+// Transport Security Layer (TLS)
+// Copyright (c) 2003-2004 Carlos Guzman Alvarez
+
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
using System;
using System.Collections;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+using System.Threading;
using Mono.Security.Protocol.Tls.Handshake;
#endregion
- public class SslClientStream : Stream, IDisposable
+ public class SslClientStream : SslStreamBase
{
#region Internal Events
#endregion
- #region Fields
-
- private Stream innerStream;
- private BufferedStream inputBuffer;
- private ClientContext context;
- private ClientRecordProtocol protocol;
- private bool ownsStream;
- private bool disposed;
- private bool checkCertRevocationStatus;
- private object read;
- private object write;
-
- #endregion
-
#region Properties
- public override bool CanRead
+ // required by HttpsClientStream for proxy support
+ internal Stream InputBuffer
{
- get { return this.innerStream.CanRead; }
+ get { return base.inputBuffer; }
}
- public override bool CanSeek
+ public X509CertificateCollection ClientCertificates
{
- get { return false; }
+ get { return this.context.ClientSettings.Certificates; }
}
- public override bool CanWrite
- {
- get { return this.innerStream.CanWrite; }
- }
-
- public override long Length
- {
- get { throw new NotSupportedException(); }
- }
-
- public override long Position
- {
- get { throw new NotSupportedException(); }
- set { throw new NotSupportedException(); }
- }
-
- #endregion
-
- #region Security Properties
-
- public bool CheckCertRevocationStatus
- {
- get { return this.checkCertRevocationStatus ; }
- set { this.checkCertRevocationStatus = value; }
- }
-
- public CipherAlgorithmType CipherAlgorithm
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.Cipher.CipherAlgorithmType;
- }
-
- return CipherAlgorithmType.None;
- }
- }
-
- public int CipherStrength
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.Cipher.EffectiveKeyBits;
- }
-
- return 0;
- }
- }
-
- public X509CertificateCollection ClientCertificates
- {
- get { return this.context.ClientSettings.Certificates;}
- }
-
- public HashAlgorithmType HashAlgorithm
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.Cipher.HashAlgorithmType;
- }
-
- return HashAlgorithmType.None;
- }
- }
-
- public int HashStrength
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.Cipher.HashSize * 8;
- }
-
- return 0;
- }
- }
-
- public int KeyExchangeStrength
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.ServerSettings.Certificates[0].RSA.KeySize;
- }
-
- return 0;
- }
- }
-
- public ExchangeAlgorithmType KeyExchangeAlgorithm
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.Cipher.ExchangeAlgorithmType;
- }
-
- return ExchangeAlgorithmType.None;
- }
- }
-
- public SecurityProtocolType SecurityProtocol
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- return this.context.SecurityProtocol;
- }
-
- return 0;
- }
- }
-
- public X509Certificate SelectedClientCertificate
+ public X509Certificate SelectedClientCertificate
{
get { return this.context.ClientSettings.ClientCertificate; }
}
- public X509Certificate ServerCertificate
- {
- get
- {
- if (this.context.HandshakeState == HandshakeState.Finished)
- {
- if (this.context.ServerSettings.Certificates != null &&
- this.context.ServerSettings.Certificates.Count > 0)
- {
- return new X509Certificate(this.context.ServerSettings.Certificates[0].RawData);
- }
- }
-
- return null;
- }
- }
-
#endregion
#region Callback Properties
set { this.ClientCertSelection = value; }
}
- public PrivateKeySelectionCallback PrivateKeyCertSelectionDelegate
+ public PrivateKeySelectionCallback PrivateKeyCertSelectionDelegate
{
get { return this.PrivateKeySelection; }
set { this.PrivateKeySelection = value; }
}
-
+
#endregion
#region Constructors
string targetHost,
bool ownsStream,
SecurityProtocolType securityProtocolType,
- X509CertificateCollection clientCertificates)
+ X509CertificateCollection clientCertificates):
+ base(stream, ownsStream)
{
- if (stream == null)
- {
- throw new ArgumentNullException("stream is null.");
- }
- if (!stream.CanRead || !stream.CanWrite)
- {
- throw new ArgumentNullException("stream is not both readable and writable.");
- }
if (targetHost == null || targetHost.Length == 0)
{
throw new ArgumentNullException("targetHost is null or an empty string.");
targetHost,
clientCertificates);
- this.inputBuffer = new BufferedStream(new MemoryStream());
- this.innerStream = stream;
- this.ownsStream = ownsStream;
- this.read = String.Empty;
- this.write = String.Empty;
- this.protocol = new ClientRecordProtocol(innerStream, context);
+ this.protocol = new ClientRecordProtocol(innerStream, (ClientContext)this.context);
}
#endregion
~SslClientStream()
{
- this.Dispose(false);
+ base.Dispose(false);
}
#endregion
- #region IDisposable Methods
+ #region IDisposable Methods
- void IDisposable.Dispose()
+ protected override void Dispose(bool disposing)
{
- this.Dispose(true);
- GC.SuppressFinalize(this);
- }
+ base.Dispose(disposing);
- protected virtual void Dispose(bool disposing)
- {
- if (!this.disposed)
+ if (disposing)
{
- if (disposing)
- {
- if (this.innerStream != null)
- {
- if (this.context.HandshakeState == HandshakeState.Finished &&
- !this.context.ConnectionEnd)
- {
- // Write close notify
- this.protocol.SendAlert(AlertDescription.CloseNotify);
- }
-
- if (this.ownsStream)
- {
- // Close inner stream
- this.innerStream.Close();
- }
- }
- this.ownsStream = false;
- this.innerStream = null;
- this.ClientCertSelection = null;
- this.ServerCertValidation = null;
- this.PrivateKeySelection = null;
- }
-
- this.disposed = true;
+ this.ServerCertValidation = null;
+ this.ClientCertSelection = null;
+ this.PrivateKeySelection = null;
}
}
#endregion
- #region Methods
+ #region Handshake Methods
- public override IAsyncResult BeginRead(
- byte[] buffer,
- int offset,
- int count,
- AsyncCallback callback,
- object state)
- {
- this.checkDisposed();
-
- if (buffer == null)
- {
- throw new ArgumentNullException("buffer is a null reference.");
- }
- if (offset < 0)
- {
- throw new ArgumentOutOfRangeException("offset is less than 0.");
- }
- if (offset > buffer.Length)
- {
- throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
- }
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException("count is less than 0.");
- }
- if (count > (buffer.Length - offset))
- {
- throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
- }
+ /*
+ Client Server
- lock (this)
- {
- if (this.context.HandshakeState == HandshakeState.None)
- {
- this.NegotiateHandshake();
- }
- }
+ ClientHello -------->
+ ServerHello
+ Certificate*
+ ServerKeyExchange*
+ CertificateRequest*
+ <-------- ServerHelloDone
+ Certificate*
+ ClientKeyExchange
+ CertificateVerify*
+ [ChangeCipherSpec]
+ Finished -------->
+ [ChangeCipherSpec]
+ <-------- Finished
+ Application Data <-------> Application Data
- IAsyncResult asyncResult;
+ Fig. 1 - Message flow for a full handshake
+ */
- lock (this.read)
+ internal override IAsyncResult OnBeginNegotiateHandshake(AsyncCallback callback, object state)
+ {
+ try
{
- try
+ if (this.context.HandshakeState != HandshakeState.None)
{
- // If actual buffer is full readed reset it
- if (this.inputBuffer.Position == this.inputBuffer.Length &&
- this.inputBuffer.Length > 0)
- {
- this.resetBuffer();
- }
-
- if (!this.context.ConnectionEnd)
- {
- // Check if we have space in the middle buffer
- // if not Read next TLS record and update the inputBuffer
- while ((this.inputBuffer.Length - this.inputBuffer.Position) < count)
- {
- // Read next record and write it into the inputBuffer
- long position = this.inputBuffer.Position;
- byte[] record = this.protocol.ReceiveRecord();
-
- if (record != null && record.Length > 0)
- {
- // Write new data to the inputBuffer
- this.inputBuffer.Seek(0, SeekOrigin.End);
- this.inputBuffer.Write(record, 0, record.Length);
-
- // Restore buffer position
- this.inputBuffer.Seek(position, SeekOrigin.Begin);
- }
- else
- {
- if (record == null)
- {
- break;
- }
- }
-
- // TODO: Review if we need to check the Length
- // property of the innerStream for other types
- // of streams, to check that there are data available
- // for read
- if (this.innerStream is NetworkStream &&
- !((NetworkStream)this.innerStream).DataAvailable)
- {
- break;
- }
- }
- }
-
- asyncResult = this.inputBuffer.BeginRead(
- buffer, offset, count, callback, state);
+ this.context.Clear();
}
- catch (TlsException ex)
- {
- this.protocol.SendAlert(ex.Alert);
- this.Close();
- throw new IOException("The authentication or decryption has failed.");
- }
- catch (Exception)
- {
- throw new IOException("IO exception during read.");
- }
- }
+ // Obtain supported cipher suites
+ this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers(this.context.SecurityProtocol);
- return asyncResult;
- }
+ // Set handshake state
+ this.context.HandshakeState = HandshakeState.Started;
- public override IAsyncResult BeginWrite(
- byte[] buffer,
- int offset,
- int count,
- AsyncCallback callback,
- object state)
- {
- this.checkDisposed();
-
- if (buffer == null)
- {
- throw new ArgumentNullException("buffer is a null reference.");
+ // Send client hello
+ return this.protocol.BeginSendRecord(HandshakeType.ClientHello, callback, state);
}
- if (offset < 0)
+ catch (TlsException ex)
{
- throw new ArgumentOutOfRangeException("offset is less than 0.");
+ this.protocol.SendAlert(ex.Alert);
+
+ throw new IOException("The authentication or decryption has failed.", ex);
}
- if (offset > buffer.Length)
+ catch (Exception ex)
{
- throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
+ this.protocol.SendAlert(AlertDescription.InternalError);
+
+ throw new IOException("The authentication or decryption has failed.", ex);
}
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException("count is less than 0.");
+ }
+
+ private void SafeReceiveRecord (Stream s)
+ {
+ byte[] record = this.protocol.ReceiveRecord (s);
+ if ((record == null) || (record.Length == 0)) {
+ throw new TlsException (
+ AlertDescription.HandshakeFailiure,
+ "The server stopped the handshake.");
}
- if (count > (buffer.Length - offset))
+ }
+
+ internal override void OnNegotiateHandshakeCallback(IAsyncResult asyncResult)
+ {
+ this.protocol.EndSendRecord(asyncResult);
+
+ // Read server response
+ while (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone)
{
- throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
+ // Read next record
+ SafeReceiveRecord (this.innerStream);
+
+ // special case for abbreviated handshake where no ServerHelloDone is sent from the server
+ if (this.context.AbbreviatedHandshake && (this.context.LastHandshakeMsg == HandshakeType.ServerHello))
+ break;
}
- lock (this)
+ // the handshake is much easier if we can reuse a previous session settings
+ if (this.context.AbbreviatedHandshake)
{
- if (this.context.HandshakeState == HandshakeState.None)
+ ClientSessionCache.SetContextFromCache (this.context);
+ this.context.Negotiating.Cipher.ComputeKeys ();
+ this.context.Negotiating.Cipher.InitializeCipher ();
+
+ // Send Cipher Spec protocol
+ this.protocol.SendChangeCipherSpec ();
+
+ // Read record until server finished is received
+ while (this.context.HandshakeState != HandshakeState.Finished)
{
- this.NegotiateHandshake();
+ // If all goes well this will process messages:
+ // Change Cipher Spec
+ // Server finished
+ SafeReceiveRecord (this.innerStream);
}
- }
- IAsyncResult asyncResult;
-
- lock (this.write)
+ // Send Finished message
+ this.protocol.SendRecord (HandshakeType.Finished);
+ }
+ else
{
- try
+ // 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)
{
- // Send the buffer as a TLS record
-
- byte[] record = this.protocol.EncodeRecord(
- ContentType.ApplicationData, buffer, offset, count);
-
- asyncResult = this.innerStream.BeginWrite(
- record, 0, record.Length, callback, state);
+ clientCertificate = ((this.context.ClientSettings.Certificates != null) &&
+ (this.context.ClientSettings.Certificates.Count > 0));
+ // this works well with OpenSSL (but only for SSL3)
}
- catch (TlsException ex)
- {
- this.protocol.SendAlert(ex.Alert);
- this.Close();
- throw new IOException("The authentication or decryption has failed.");
- }
- catch (Exception)
+ if (clientCertificate)
{
- throw new IOException("IO exception during Write.");
+ this.protocol.SendRecord(HandshakeType.Certificate);
}
- }
- return asyncResult;
- }
+ // Send Client Key Exchange
+ this.protocol.SendRecord(HandshakeType.ClientKeyExchange);
- public override int EndRead(IAsyncResult asyncResult)
- {
- this.checkDisposed();
+ // Now initialize session cipher with the generated keys
+ this.context.Negotiating.Cipher.InitializeCipher();
- if (asyncResult == null)
- {
- throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
- }
+ // Send certificate verify if requested (optional)
+ if (clientCertificate && (this.context.ClientSettings.ClientCertificate != null))
+ {
+ this.protocol.SendRecord(HandshakeType.CertificateVerify);
+ }
- return this.inputBuffer.EndRead(asyncResult);
- }
+ // Send Cipher Spec protocol
+ this.protocol.SendChangeCipherSpec ();
- public override void EndWrite(IAsyncResult asyncResult)
- {
- this.checkDisposed();
+ // Send Finished message
+ this.protocol.SendRecord (HandshakeType.Finished);
- if (asyncResult == null)
- {
- throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
+ // 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);
+ }
}
- this.innerStream.EndWrite (asyncResult);
- }
-
- public override void Close()
- {
- ((IDisposable)this).Dispose();
- }
-
- public override void Flush()
- {
- this.checkDisposed();
-
- this.innerStream.Flush();
- }
-
- public int Read(byte[] buffer)
- {
- return this.Read(buffer, 0, buffer.Length);
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- IAsyncResult res = this.BeginRead(buffer, offset, count, null, null);
-
- return this.EndRead(res);
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotSupportedException();
- }
-
- public override void SetLength(long value)
- {
- throw new NotSupportedException();
- }
-
- public void Write(byte[] buffer)
- {
- this.Write(buffer, 0, buffer.Length);
- }
+ // Reset Handshake messages information
+ this.context.HandshakeMessages.Reset ();
- public override void Write(byte[] buffer, int offset, int count)
- {
- IAsyncResult res = this.BeginWrite (buffer, offset, count, null, null);
+ // Clear Key Info
+ this.context.ClearKeyInfo();
- this.EndWrite(res);
}
#endregion
- #region Misc Methods
-
- private void resetBuffer()
- {
- this.inputBuffer.SetLength(0);
- this.inputBuffer.Position = 0;
- }
+ #region Event Methods
- private void checkDisposed()
+ internal override X509Certificate OnLocalCertificateSelection(X509CertificateCollection clientCertificates, X509Certificate serverCertificate, string targetHost, X509CertificateCollection serverRequestedCertificates)
{
- if (this.disposed)
+ if (this.ClientCertSelection != null)
{
- throw new ObjectDisposedException("The SslClientStream is closed.");
+ return this.ClientCertSelection(
+ clientCertificates,
+ serverCertificate,
+ targetHost,
+ serverRequestedCertificates);
}
- }
-
- #endregion
-
- #region Handsake Methods
-
- /*
- Client Server
-
- ClientHello -------->
- ServerHello
- Certificate*
- ServerKeyExchange*
- CertificateRequest*
- <-------- ServerHelloDone
- Certificate*
- ClientKeyExchange
- CertificateVerify*
- [ChangeCipherSpec]
- Finished -------->
- [ChangeCipherSpec]
- <-------- Finished
- Application Data <-------> Application Data
- Fig. 1 - Message flow for a full handshake
- */
-
- internal void NegotiateHandshake()
+ return null;
+ }
+
+ internal override bool OnRemoteCertificateValidation(X509Certificate certificate, int[] errors)
{
- lock (this)
+ if (this.ServerCertValidation != null)
{
- try
- {
- if (this.context.HandshakeState != HandshakeState.None)
- {
- this.context.Clear();
- }
-
- // Obtain supported cipher suites
- this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers(this.context.SecurityProtocol);
-
- // Send client hello
- this.protocol.SendRecord(HandshakeType.ClientHello);
-
- // Read server response
- while (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone)
- {
- // Read next record
- this.protocol.ReceiveRecord();
- }
-
- // Send client certificate if requested
- if (this.context.ServerSettings.CertificateRequest)
- {
- this.protocol.SendRecord(HandshakeType.Certificate);
- }
-
- // Send Client Key Exchange
- this.protocol.SendRecord(HandshakeType.ClientKeyExchange);
-
- // Now initialize session cipher with the generated keys
- this.context.Cipher.InitializeCipher();
-
- // Send certificate verify if requested
- if (this.context.ServerSettings.CertificateRequest)
- {
- this.protocol.SendRecord(HandshakeType.CertificateVerify);
- }
-
- // Send Cipher Spec protocol
- this.protocol.SendChangeCipherSpec();
-
- // 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
- this.protocol.ReceiveRecord();
- }
-
- // Clear Key Info
- this.context.ClearKeyInfo();
- }
- catch (TlsException ex)
- {
- this.protocol.SendAlert(ex.Alert);
- this.Close();
-
- throw new IOException("The authentication or decryption has failed.");
- }
- catch (Exception)
- {
- this.protocol.SendAlert(AlertDescription.InternalError);
- this.Close();
-
- throw new IOException("The authentication or decryption has failed.");
- }
+ return this.ServerCertValidation(certificate, errors);
}
- }
- #endregion
-
- #region Event Methods
+ return (errors != null && errors.Length == 0);
+ }
internal virtual bool RaiseServerCertificateValidation(
X509Certificate certificate,
int[] certificateErrors)
{
- if (this.ServerCertValidation != null)
- {
- return this.ServerCertValidation(certificate, certificateErrors);
- }
-
- return (certificateErrors != null && certificateErrors.Length == 0);
+ return base.RaiseRemoteCertificateValidation(certificate, certificateErrors);
}
internal X509Certificate RaiseClientCertificateSelection(
string targetHost,
X509CertificateCollection serverRequestedCertificates)
{
- if (this.ClientCertSelection != null)
- {
- return this.ClientCertSelection(
- clientCertificates,
- serverCertificate,
- targetHost,
- serverRequestedCertificates);
- }
-
- return null;
+ return base.RaiseLocalCertificateSelection(clientCertificates, serverCertificate, targetHost, serverRequestedCertificates);
}
- internal AsymmetricAlgorithm RaisePrivateKeySelection(
- X509Certificate certificate,
- string targetHost)
+ internal override AsymmetricAlgorithm OnLocalPrivateKeySelection(X509Certificate certificate, string targetHost)
{
if (this.PrivateKeySelection != null)
{
return null;
}
+ internal AsymmetricAlgorithm RaisePrivateKeySelection(
+ X509Certificate certificate,
+ string targetHost)
+ {
+ return base.RaiseLocalPrivateKeySelection(certificate, targetHost);
+ }
+
#endregion
}
-}
\ No newline at end of file
+}