1 // Transport Security Layer (TLS)
2 // Copyright (c) 2003-2004 Carlos Guzman Alvarez
5 // Permission is hereby granted, free of charge, to any person obtaining
6 // a copy of this software and associated documentation files (the
7 // "Software"), to deal in the Software without restriction, including
8 // without limitation the rights to use, copy, modify, merge, publish,
9 // distribute, sublicense, and/or sell copies of the Software, and to
10 // permit persons to whom the Software is furnished to do so, subject to
11 // the following conditions:
13 // The above copyright notice and this permission notice shall be
14 // included in all copies or substantial portions of the Software.
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 using System.Collections;
29 using System.Net.Sockets;
30 using System.Security.Cryptography;
31 using System.Security.Cryptography.X509Certificates;
32 using System.Threading;
34 using Mono.Security.Protocol.Tls.Handshake;
36 namespace Mono.Security.Protocol.Tls
40 public delegate bool CertificateValidationCallback(
41 X509Certificate certificate,
42 int[] certificateErrors);
44 public delegate X509Certificate CertificateSelectionCallback(
45 X509CertificateCollection clientCertificates,
46 X509Certificate serverCertificate,
48 X509CertificateCollection serverRequestedCertificates);
50 public delegate AsymmetricAlgorithm PrivateKeySelectionCallback(
51 X509Certificate certificate,
54 delegate int ReadDelegate (byte [] buffer, int offset, int count);
57 public class SslClientStream : Stream, IDisposable
59 #region Internal Events
61 internal event CertificateValidationCallback ServerCertValidation;
62 internal event CertificateSelectionCallback ClientCertSelection;
63 internal event PrivateKeySelectionCallback PrivateKeySelection;
69 private Stream innerStream;
70 private MemoryStream inputBuffer;
71 private ClientContext context;
72 private ClientRecordProtocol protocol;
73 private bool ownsStream;
74 private bool disposed;
75 private bool checkCertRevocationStatus;
76 private object negotiate;
79 private ReadDelegate rd;
85 public override bool CanRead
87 get { return this.innerStream.CanRead; }
90 public override bool CanSeek
95 public override bool CanWrite
97 get { return this.innerStream.CanWrite; }
100 public override long Length
102 get { throw new NotSupportedException(); }
105 public override long Position
107 get { throw new NotSupportedException(); }
108 set { throw new NotSupportedException(); }
111 // required by HttpsClientStream for proxy support
112 internal Stream InputBuffer {
113 get { return inputBuffer; }
117 #region Security Properties
119 public bool CheckCertRevocationStatus
121 get { return this.checkCertRevocationStatus ; }
122 set { this.checkCertRevocationStatus = value; }
125 public CipherAlgorithmType CipherAlgorithm
129 if (this.context.HandshakeState == HandshakeState.Finished)
131 return this.context.Cipher.CipherAlgorithmType;
134 return CipherAlgorithmType.None;
138 public int CipherStrength
142 if (this.context.HandshakeState == HandshakeState.Finished)
144 return this.context.Cipher.EffectiveKeyBits;
151 public X509CertificateCollection ClientCertificates
153 get { return this.context.ClientSettings.Certificates;}
156 public HashAlgorithmType HashAlgorithm
160 if (this.context.HandshakeState == HandshakeState.Finished)
162 return this.context.Cipher.HashAlgorithmType;
165 return HashAlgorithmType.None;
169 public int HashStrength
173 if (this.context.HandshakeState == HandshakeState.Finished)
175 return this.context.Cipher.HashSize * 8;
182 public int KeyExchangeStrength
186 if (this.context.HandshakeState == HandshakeState.Finished)
188 return this.context.ServerSettings.Certificates[0].RSA.KeySize;
195 public ExchangeAlgorithmType KeyExchangeAlgorithm
199 if (this.context.HandshakeState == HandshakeState.Finished)
201 return this.context.Cipher.ExchangeAlgorithmType;
204 return ExchangeAlgorithmType.None;
208 public SecurityProtocolType SecurityProtocol
212 if (this.context.HandshakeState == HandshakeState.Finished)
214 return this.context.SecurityProtocol;
221 public X509Certificate SelectedClientCertificate
223 get { return this.context.ClientSettings.ClientCertificate; }
226 public X509Certificate ServerCertificate
230 if (this.context.HandshakeState == HandshakeState.Finished)
232 if (this.context.ServerSettings.Certificates != null &&
233 this.context.ServerSettings.Certificates.Count > 0)
235 return new X509Certificate(this.context.ServerSettings.Certificates[0].RawData);
243 // this is used by Mono's certmgr tool to download certificates
244 internal Mono.Security.X509.X509CertificateCollection ServerCertificates {
245 get { return context.ServerSettings.Certificates; }
250 #region Callback Properties
252 public CertificateValidationCallback ServerCertValidationDelegate
254 get { return this.ServerCertValidation; }
255 set { this.ServerCertValidation = value; }
258 public CertificateSelectionCallback ClientCertSelectionDelegate
260 get { return this.ClientCertSelection; }
261 set { this.ClientCertSelection = value; }
264 public PrivateKeySelectionCallback PrivateKeyCertSelectionDelegate
266 get { return this.PrivateKeySelection; }
267 set { this.PrivateKeySelection = value; }
274 public SslClientStream(
279 stream, targetHost, ownsStream,
280 SecurityProtocolType.Default, null)
284 public SslClientStream(
287 X509Certificate clientCertificate)
289 stream, targetHost, false, SecurityProtocolType.Default,
290 new X509CertificateCollection(new X509Certificate[]{clientCertificate}))
294 public SslClientStream(
297 X509CertificateCollection clientCertificates) :
299 stream, targetHost, false, SecurityProtocolType.Default,
304 public SslClientStream(
308 SecurityProtocolType securityProtocolType)
310 stream, targetHost, ownsStream, securityProtocolType,
311 new X509CertificateCollection())
315 public SslClientStream(
319 SecurityProtocolType securityProtocolType,
320 X509CertificateCollection clientCertificates)
324 throw new ArgumentNullException("stream is null.");
326 if (!stream.CanRead || !stream.CanWrite)
328 throw new ArgumentNullException("stream is not both readable and writable.");
330 if (targetHost == null || targetHost.Length == 0)
332 throw new ArgumentNullException("targetHost is null or an empty string.");
335 this.context = new ClientContext(
337 securityProtocolType,
341 this.inputBuffer = new MemoryStream();
342 this.innerStream = stream;
343 this.ownsStream = ownsStream;
344 this.negotiate = new object ();
345 this.read = new object ();
346 this.write = new object ();
347 this.protocol = new ClientRecordProtocol(innerStream, context);
361 #region IDisposable Methods
363 void IDisposable.Dispose()
366 GC.SuppressFinalize(this);
369 protected virtual void Dispose(bool disposing)
375 if (this.innerStream != null)
377 if (this.context.HandshakeState == HandshakeState.Finished &&
378 !this.context.ConnectionEnd)
380 // Write close notify
381 this.protocol.SendAlert(AlertDescription.CloseNotify);
386 // Close inner stream
387 this.innerStream.Close();
390 this.ownsStream = false;
391 this.innerStream = null;
392 this.ClientCertSelection = null;
393 this.ServerCertValidation = null;
394 this.PrivateKeySelection = null;
397 this.disposed = true;
405 public override IAsyncResult BeginRead(
409 AsyncCallback callback,
412 this.checkDisposed ();
416 throw new ArgumentNullException("buffer is a null reference.");
420 throw new ArgumentOutOfRangeException("offset is less than 0.");
422 if (offset > buffer.Length)
424 throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
428 throw new ArgumentOutOfRangeException("count is less than 0.");
430 if (count > (buffer.Length - offset))
432 throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
435 if (this.context.HandshakeState == HandshakeState.None)
437 // Note: Async code may have problem if they can't ensure that
438 // the Negotiate phase isn't done during a read operation.
439 // System.Net.HttpWebRequest protects itself from that problem
440 lock (this.negotiate)
442 if (this.context.HandshakeState == HandshakeState.None)
444 this.NegotiateHandshake();
449 IAsyncResult asyncResult = null;
455 // If actual buffer is full readed reset it
456 if (this.inputBuffer.Position == this.inputBuffer.Length &&
457 this.inputBuffer.Length > 0)
462 if (!this.context.ConnectionEnd)
464 if ((this.inputBuffer.Length == this.inputBuffer.Position) && (count > 0))
\r
466 // bigger than max record length for SSL/TLS
\r
467 byte[] recbuf = new byte[16384];
\r
469 // this will read data from the network until we have (at least) one
470 // record to send back to the caller
\r
471 this.innerStream.BeginRead (recbuf, 0, recbuf.Length,
472 new AsyncCallback (NetworkReadCallback), recbuf);
\r
474 if (!recordEvent.WaitOne (300000, false)) // 5 minutes
\r
477 DebugHelper.WriteLine ("TIMEOUT length {0}, position {1}, count {2} - {3}\n{4}",
478 this.inputBuffer.Length, this.inputBuffer.Position, count, GetHashCode (),
479 Environment.StackTrace);
\r
480 throw new TlsException (AlertDescription.InternalError);
\r
485 // return the record(s) to the caller
486 rd = new ReadDelegate (this.inputBuffer.Read);
487 asyncResult = rd.BeginInvoke (buffer, offset, count, callback, state);
489 catch (TlsException ex)
491 this.protocol.SendAlert(ex.Alert);
494 throw new IOException("The authentication or decryption has failed.");
498 throw new IOException("IO exception during read.");
506 private ManualResetEvent recordEvent = new ManualResetEvent (false);
\r
507 private MemoryStream recordStream = new MemoryStream ();
\r
509 // read encrypted data until we have enough to decrypt (at least) one
510 // record and return are the records (may be more than one) we have
\r
511 private void NetworkReadCallback (IAsyncResult result)
\r
514 recordEvent.Set ();
\r
518 byte[] recbuf = (byte[])result.AsyncState;
\r
519 int n = innerStream.EndRead (result);
522 // add the just received data to the waiting data
\r
523 recordStream.Write (recbuf, 0, n);
\r
526 bool dataToReturn = false;
\r
527 long pos = recordStream.Position;
529 recordStream.Position = 0;
\r
530 byte[] record = null;
532 // don't try to decode record unless we have at least 5 bytes
\r
533 // i.e. type (1), protocol (2) and length (2)
\r
534 if (recordStream.Length >= 5)
\r
536 record = this.protocol.ReceiveRecord (recordStream);
\r
539 // a record of 0 length is valid (and there may be more record after it)
540 while (record != null)
\r
542 // we probably received more stuff after the record, and we must keep it!
\r
543 long remainder = recordStream.Length - recordStream.Position;
\r
544 byte[] outofrecord = null;
\r
547 outofrecord = new byte[remainder];
\r
548 recordStream.Read (outofrecord, 0, outofrecord.Length);
\r
551 long position = this.inputBuffer.Position;
\r
553 if (record.Length > 0)
555 // Write new data to the inputBuffer
\r
556 this.inputBuffer.Seek (0, SeekOrigin.End);
\r
557 this.inputBuffer.Write (record, 0, record.Length);
\r
559 // Restore buffer position
\r
560 this.inputBuffer.Seek (position, SeekOrigin.Begin);
564 recordStream.SetLength (0);
\r
569 recordStream.Write (outofrecord, 0, outofrecord.Length);
\r
570 // type (1), protocol (2) and length (2)
\r
571 if (recordStream.Length >= 5)
\r
573 // try to see if another record is available
\r
574 recordStream.Position = 0;
\r
575 record = this.protocol.ReceiveRecord (recordStream);
\r
576 if (record == null)
\r
577 pos = recordStream.Length;
\r
586 if (!dataToReturn && (n > 0))
\r
588 // there is no record to return to caller and (possibly) more data waiting
\r
589 // so continue reading from network (and appending to stream)
\r
590 recordStream.Position = recordStream.Length;
\r
591 this.innerStream.BeginRead (recbuf, 0, recbuf.Length,
592 new AsyncCallback (NetworkReadCallback), recbuf);
\r
596 // we have record(s) to return -or- no more available to read from network
597 // reset position for further reading
\r
598 recordStream.Position = pos;
\r
599 recordEvent.Set ();
\r
603 public override IAsyncResult BeginWrite(
607 AsyncCallback callback,
610 this.checkDisposed();
614 throw new ArgumentNullException("buffer is a null reference.");
618 throw new ArgumentOutOfRangeException("offset is less than 0.");
620 if (offset > buffer.Length)
622 throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
626 throw new ArgumentOutOfRangeException("count is less than 0.");
628 if (count > (buffer.Length - offset))
630 throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
633 if (this.context.HandshakeState == HandshakeState.None)
635 lock (this.negotiate)
637 if (this.context.HandshakeState == HandshakeState.None)
639 this.NegotiateHandshake();
644 IAsyncResult asyncResult;
650 // Send the buffer as a TLS record
652 byte[] record = this.protocol.EncodeRecord(
653 ContentType.ApplicationData, buffer, offset, count);
655 asyncResult = this.innerStream.BeginWrite(
656 record, 0, record.Length, callback, state);
658 catch (TlsException ex)
660 this.protocol.SendAlert(ex.Alert);
663 throw new IOException("The authentication or decryption has failed.");
667 throw new IOException("IO exception during Write.");
674 public override int EndRead(IAsyncResult asyncResult)
676 this.checkDisposed();
678 if (asyncResult == null)
680 throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
683 recordEvent.Reset ();
684 return this.rd.EndInvoke (asyncResult);
687 public override void EndWrite(IAsyncResult asyncResult)
689 this.checkDisposed();
691 if (asyncResult == null)
693 throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
696 this.innerStream.EndWrite (asyncResult);
699 public override void Close()
701 ((IDisposable)this).Dispose();
704 public override void Flush()
706 this.checkDisposed();
708 this.innerStream.Flush();
711 public int Read(byte[] buffer)
713 return this.Read(buffer, 0, buffer.Length);
716 public override int Read(byte[] buffer, int offset, int count)
718 IAsyncResult res = this.BeginRead(buffer, offset, count, null, null);
720 return this.EndRead(res);
723 public override long Seek(long offset, SeekOrigin origin)
725 throw new NotSupportedException();
728 public override void SetLength(long value)
730 throw new NotSupportedException();
733 public void Write(byte[] buffer)
735 this.Write(buffer, 0, buffer.Length);
738 public override void Write(byte[] buffer, int offset, int count)
740 IAsyncResult res = this.BeginWrite (buffer, offset, count, null, null);
749 private void resetBuffer()
751 this.inputBuffer.SetLength(0);
752 this.inputBuffer.Position = 0;
755 private void checkDisposed()
759 throw new ObjectDisposedException("The SslClientStream is closed.");
765 #region Handsake Methods
770 ClientHello -------->
775 <-------- ServerHelloDone
783 Application Data <-------> Application Data
785 Fig. 1 - Message flow for a full handshake
788 internal void NegotiateHandshake()
792 if (this.context.HandshakeState != HandshakeState.None)
794 this.context.Clear();
797 // Obtain supported cipher suites
798 this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers(this.context.SecurityProtocol);
801 this.protocol.SendRecord(HandshakeType.ClientHello);
803 // Read server response
804 while (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone)
806 // Read next record
\r
807 this.protocol.ReceiveRecord (this.innerStream);
810 // Send client certificate if requested
811 // even if the server ask for it it _may_ still be optional
812 bool clientCertificate = this.context.ServerSettings.CertificateRequest;
814 // NOTE: sadly SSL3 and TLS1 differs in how they handle this and
815 // the current design doesn't allow a very cute way to handle
816 // SSL3 alert warning for NoCertificate (41).
817 if (this.context.SecurityProtocol == SecurityProtocolType.Ssl3)
819 clientCertificate = ((this.context.ClientSettings.Certificates != null) &&
820 (this.context.ClientSettings.Certificates.Count > 0));
821 // this works well with OpenSSL (but only for SSL3)
824 if (clientCertificate)
826 this.protocol.SendRecord(HandshakeType.Certificate);
829 // Send Client Key Exchange
830 this.protocol.SendRecord(HandshakeType.ClientKeyExchange);
832 // Now initialize session cipher with the generated keys
833 this.context.Cipher.InitializeCipher();
835 // Send certificate verify if requested (optional)
836 if (clientCertificate && (this.context.ClientSettings.ClientCertificate != null))
838 this.protocol.SendRecord(HandshakeType.CertificateVerify);
841 // Send Cipher Spec protocol
842 this.protocol.SendChangeCipherSpec();
844 // Read record until server finished is received
845 while (this.context.HandshakeState != HandshakeState.Finished)
847 // If all goes well this will process messages:
848 // Change Cipher Spec
850 this.protocol.ReceiveRecord (this.innerStream);
854 this.context.ClearKeyInfo();
856 catch (TlsException ex)
858 this.protocol.SendAlert(ex.Alert);
861 throw new IOException("The authentication or decryption has failed.");
865 this.protocol.SendAlert(AlertDescription.InternalError);
868 throw new IOException("The authentication or decryption has failed.");
874 #region Event Methods
876 internal virtual bool RaiseServerCertificateValidation(
877 X509Certificate certificate,
878 int[] certificateErrors)
880 if (this.ServerCertValidation != null)
882 return this.ServerCertValidation(certificate, certificateErrors);
885 return (certificateErrors != null && certificateErrors.Length == 0);
888 internal X509Certificate RaiseClientCertificateSelection(
889 X509CertificateCollection clientCertificates,
890 X509Certificate serverCertificate,
892 X509CertificateCollection serverRequestedCertificates)
894 if (this.ClientCertSelection != null)
896 return this.ClientCertSelection(
900 serverRequestedCertificates);
906 internal AsymmetricAlgorithm RaisePrivateKeySelection(
907 X509Certificate certificate,
910 if (this.PrivateKeySelection != null)
912 return this.PrivateKeySelection(certificate, targetHost);