1 // Transport Security Layer (TLS)
2 // Copyright (c) 2003-2004 Carlos Guzman Alvarez
3 // Copyright (C) 2006-2007 Novell, Inc (http://www.novell.com)
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
;
28 using System
.Threading
;
30 using Mono
.Security
.Protocol
.Tls
.Handshake
;
32 namespace Mono
.Security
.Protocol
.Tls
34 internal abstract class RecordProtocol
38 private static ManualResetEvent record_processing
= new ManualResetEvent (true);
40 protected Stream innerStream
;
41 protected Context context
;
47 public Context Context
49 get { return this.context; }
50 set { this.context = value; }
57 public RecordProtocol(Stream innerStream
, Context context
)
59 this.innerStream
= innerStream
;
60 this.context
= context
;
61 this.context
.RecordProtocol
= this;
66 #region Abstract Methods
68 public virtual void SendRecord(HandshakeType type
)
71 IAsyncResult ar
= this.BeginSendRecord(type
, null, null);
73 this.EndSendRecord(ar
);
77 protected abstract void ProcessHandshakeMessage(TlsStream handMsg
);
79 protected virtual void ProcessChangeCipherSpec ()
81 Context ctx
= this.Context
;
83 // Reset sequence numbers
84 ctx
.ReadSequenceNumber
= 0;
86 if (ctx
is ClientContext
) {
87 ctx
.EndSwitchingSecurityParameters (true);
89 ctx
.StartSwitchingSecurityParameters (false);
93 public virtual HandshakeMessage
GetMessage(HandshakeType type
)
95 throw new NotSupportedException();
100 #region Receive Record Async Result
101 private class ReceiveRecordAsyncResult
: IAsyncResult
103 private object locker
= new object ();
104 private AsyncCallback _userCallback
;
105 private object _userState
;
106 private Exception _asyncException
;
107 private ManualResetEvent handle
;
108 private byte[] _resultingBuffer
;
109 private Stream _record
;
110 private bool completed
;
112 private byte[] _initialBuffer
;
114 public ReceiveRecordAsyncResult(AsyncCallback userCallback
, object userState
, byte[] initialBuffer
, Stream record
)
116 _userCallback
= userCallback
;
117 _userState
= userState
;
118 _initialBuffer
= initialBuffer
;
124 get { return _record; }
127 public byte[] ResultingBuffer
129 get { return _resultingBuffer; }
132 public byte[] InitialBuffer
134 get { return _initialBuffer; }
137 public object AsyncState
139 get { return _userState; }
142 public Exception AsyncException
144 get { return _asyncException; }
147 public bool CompletedWithError
151 return false; // Perhaps throw InvalidOperationExcetion?
153 return null != _asyncException
;
157 public WaitHandle AsyncWaitHandle
162 handle
= new ManualResetEvent (completed
);
169 public bool CompletedSynchronously
171 get { return false; }
174 public bool IsCompleted
183 private void SetComplete(Exception ex
, byte[] resultingBuffer
)
190 _asyncException
= ex
;
191 _resultingBuffer
= resultingBuffer
;
195 if (_userCallback
!= null)
196 _userCallback
.BeginInvoke (this, null, null);
200 public void SetComplete(Exception ex
)
202 SetComplete(ex
, null);
205 public void SetComplete(byte[] resultingBuffer
)
207 SetComplete(null, resultingBuffer
);
210 public void SetComplete()
212 SetComplete(null, null);
217 #region Receive Record Async Result
218 private class SendRecordAsyncResult
: IAsyncResult
220 private object locker
= new object ();
221 private AsyncCallback _userCallback
;
222 private object _userState
;
223 private Exception _asyncException
;
224 private ManualResetEvent handle
;
225 private HandshakeMessage _message
;
226 private bool completed
;
228 public SendRecordAsyncResult(AsyncCallback userCallback
, object userState
, HandshakeMessage message
)
230 _userCallback
= userCallback
;
231 _userState
= userState
;
235 public HandshakeMessage Message
237 get { return _message; }
240 public object AsyncState
242 get { return _userState; }
245 public Exception AsyncException
247 get { return _asyncException; }
250 public bool CompletedWithError
254 return false; // Perhaps throw InvalidOperationExcetion?
256 return null != _asyncException
;
260 public WaitHandle AsyncWaitHandle
265 handle
= new ManualResetEvent (completed
);
272 public bool CompletedSynchronously
274 get { return false; }
277 public bool IsCompleted
286 public void SetComplete(Exception ex
)
296 if (_userCallback
!= null)
297 _userCallback
.BeginInvoke (this, null, null);
299 _asyncException
= ex
;
303 public void SetComplete()
310 #region Reveive Record Methods
312 public IAsyncResult
BeginReceiveRecord(Stream record
, AsyncCallback callback
, object state
)
314 if (this.context
.ConnectionEnd
)
316 throw new TlsException(
317 AlertDescription
.InternalError
,
318 "The session is finished and it's no longer valid.");
321 record_processing
.Reset ();
322 byte[] recordTypeBuffer
= new byte[1];
324 ReceiveRecordAsyncResult internalResult
= new ReceiveRecordAsyncResult(callback
, state
, recordTypeBuffer
, record
);
326 record
.BeginRead(internalResult
.InitialBuffer
, 0, internalResult
.InitialBuffer
.Length
, new AsyncCallback(InternalReceiveRecordCallback
), internalResult
);
328 return internalResult
;
331 private void InternalReceiveRecordCallback(IAsyncResult asyncResult
)
333 ReceiveRecordAsyncResult internalResult
= asyncResult
.AsyncState
as ReceiveRecordAsyncResult
;
334 Stream record
= internalResult
.Record
;
339 int bytesRead
= internalResult
.Record
.EndRead(asyncResult
);
341 //We're at the end of the stream. Time to bail.
344 internalResult
.SetComplete((byte[])null);
348 // Try to read the Record Content Type
349 int type
= internalResult
.InitialBuffer
[0];
351 // Set last handshake message received to None
352 this.context
.LastHandshakeMsg
= HandshakeType
.ClientHello
;
354 ContentType contentType
= (ContentType
)type
;
355 byte[] buffer
= this.ReadRecordBuffer(type
, record
);
358 // record incomplete (at the moment)
359 internalResult
.SetComplete((byte[])null);
363 // Decrypt message contents if needed
364 if (contentType
== ContentType
.Alert
&& buffer
.Length
== 2)
367 else if ((this.Context
.Read
!= null) && (this.Context
.Read
.Cipher
!= null))
369 buffer
= this.decryptRecordFragment (contentType
, buffer
);
370 DebugHelper
.WriteLine ("Decrypted record data", buffer
);
376 case ContentType
.Alert
:
377 this.ProcessAlert((AlertLevel
)buffer
[0], (AlertDescription
)buffer
[1]);
380 // don't reprocess that memory block
381 record
.SetLength (0);
386 case ContentType
.ChangeCipherSpec
:
387 this.ProcessChangeCipherSpec();
390 case ContentType
.ApplicationData
:
393 case ContentType
.Handshake
:
394 TlsStream message
= new TlsStream (buffer
);
397 this.ProcessHandshakeMessage(message
);
401 case (ContentType
)0x80:
402 this.context
.HandshakeMessages
.Write (buffer
);
406 throw new TlsException(
407 AlertDescription
.UnexpectedMessage
,
408 "Unknown record received from server.");
411 internalResult
.SetComplete(buffer
);
415 internalResult
.SetComplete(ex
);
420 public byte[] EndReceiveRecord(IAsyncResult asyncResult
)
422 ReceiveRecordAsyncResult internalResult
= asyncResult
as ReceiveRecordAsyncResult
;
424 if (null == internalResult
)
425 throw new ArgumentException("Either the provided async result is null or was not created by this RecordProtocol.");
427 if (!internalResult
.IsCompleted
)
428 internalResult
.AsyncWaitHandle
.WaitOne();
430 if (internalResult
.CompletedWithError
)
431 throw internalResult
.AsyncException
;
433 byte[] result
= internalResult
.ResultingBuffer
;
434 record_processing
.Set ();
438 public byte[] ReceiveRecord(Stream record
)
441 IAsyncResult ar
= this.BeginReceiveRecord(record
, null, null);
442 return this.EndReceiveRecord(ar
);
446 private byte[] ReadRecordBuffer (int contentType
, Stream record
)
451 return this.ReadClientHelloV2(record
);
454 if (!Enum
.IsDefined(typeof(ContentType
), (ContentType
)contentType
))
456 throw new TlsException(AlertDescription
.DecodeError
);
458 return this.ReadStandardRecordBuffer(record
);
462 private byte[] ReadClientHelloV2 (Stream record
)
464 int msgLength
= record
.ReadByte ();
465 // process further only if the whole record is available
466 if (record
.CanSeek
&& (msgLength
+ 1 > record
.Length
))
471 byte[] message
= new byte[msgLength
];
472 record
.Read (message
, 0, msgLength
);
474 int msgType
= message
[0];
477 throw new TlsException(AlertDescription
.DecodeError
);
479 int protocol
= (message
[1] << 8 | message
[2]);
480 int cipherSpecLength
= (message
[3] << 8 | message
[4]);
481 int sessionIdLength
= (message
[5] << 8 | message
[6]);
482 int challengeLength
= (message
[7] << 8 | message
[8]);
483 int length
= (challengeLength
> 32) ? 32 : challengeLength
;
486 byte[] cipherSpecV2
= new byte[cipherSpecLength
];
487 Buffer
.BlockCopy (message
, 9, cipherSpecV2
, 0, cipherSpecLength
);
490 byte[] sessionId
= new byte[sessionIdLength
];
491 Buffer
.BlockCopy (message
, 9 + cipherSpecLength
, sessionId
, 0, sessionIdLength
);
494 byte[] challenge
= new byte[challengeLength
];
495 Buffer
.BlockCopy (message
, 9 + cipherSpecLength
+ sessionIdLength
, challenge
, 0, challengeLength
);
497 if (challengeLength
< 16 || cipherSpecLength
== 0 || (cipherSpecLength
% 3) != 0)
499 throw new TlsException(AlertDescription
.DecodeError
);
502 // Updated the Session ID
503 if (sessionId
.Length
> 0)
505 this.context
.SessionId
= sessionId
;
508 // Update the protocol version
509 this.Context
.ChangeProtocol((short)protocol
);
511 // Select the Cipher suite
512 this.ProcessCipherSpecV2Buffer(this.Context
.SecurityProtocol
, cipherSpecV2
);
514 // Updated the Client Random
515 this.context
.ClientRandom
= new byte [32]; // Always 32
516 // 1. if challenge is bigger than 32 bytes only use the last 32 bytes
517 // 2. right justify (0) challenge in ClientRandom if less than 32
518 Buffer
.BlockCopy (challenge
, challenge
.Length
- length
, this.context
.ClientRandom
, 32 - length
, length
);
521 this.context
.LastHandshakeMsg
= HandshakeType
.ClientHello
;
522 this.context
.ProtocolNegotiated
= true;
527 private byte[] ReadStandardRecordBuffer (Stream record
)
529 byte[] header
= new byte[4];
530 if (record
.Read (header
, 0, 4) != 4)
531 throw new TlsException ("buffer underrun");
533 short protocol
= (short)((header
[0] << 8) | header
[1]);
534 short length
= (short)((header
[2] << 8) | header
[3]);
536 // process further only if the whole record is available
537 // note: the first 5 bytes aren't part of the length
538 if (record
.CanSeek
&& (length
+ 5 > record
.Length
))
544 int totalReceived
= 0;
545 byte[] buffer
= new byte[length
];
546 while (totalReceived
!= length
)
548 int justReceived
= record
.Read(buffer
, totalReceived
, buffer
.Length
- totalReceived
);
550 //Make sure we get some data so we don't end up in an infinite loop here before shutdown.
551 if (0 == justReceived
)
553 throw new TlsException(AlertDescription
.CloseNotify
, "Received 0 bytes from stream. It must be closed.");
556 totalReceived
+= justReceived
;
559 // Check that the message has a valid protocol version
560 if (protocol
!= this.context
.Protocol
&& this.context
.ProtocolNegotiated
)
562 throw new TlsException(
563 AlertDescription
.ProtocolVersion
, "Invalid protocol version on message received");
566 DebugHelper
.WriteLine("Record data", buffer
);
571 private void ProcessAlert(AlertLevel alertLevel
, AlertDescription alertDesc
)
575 case AlertLevel
.Fatal
:
576 throw new TlsException(alertLevel
, alertDesc
);
578 case AlertLevel
.Warning
:
582 case AlertDescription
.CloseNotify
:
583 this.context
.ConnectionEnd
= true;
592 #region Send Alert Methods
594 public void SendAlert(AlertDescription description
)
596 this.SendAlert(new Alert(description
));
599 public void SendAlert(
601 AlertDescription description
)
603 this.SendAlert(new Alert(level
, description
));
606 public void SendAlert(Alert alert
)
609 AlertDescription description
;
613 DebugHelper
.WriteLine(">>>> Write Alert NULL");
614 level
= AlertLevel
.Fatal
;
615 description
= AlertDescription
.InternalError
;
618 DebugHelper
.WriteLine(">>>> Write Alert ({0}|{1})", alert
.Description
, alert
.Message
);
620 description
= alert
.Description
;
621 close
= alert
.IsCloseNotify
;
625 this.SendRecord (ContentType
.Alert
, new byte[2] { (byte) level, (byte) description }
);
629 this.context
.ConnectionEnd
= true;
635 #region Send Record Methods
637 public void SendChangeCipherSpec()
639 DebugHelper
.WriteLine(">>>> Write Change Cipher Spec");
641 // Send Change Cipher Spec message with the current cipher
642 // or as plain text if this is the initial negotiation
643 this.SendRecord(ContentType
.ChangeCipherSpec
, new byte[] {1}
);
645 Context ctx
= this.context
;
647 // Reset sequence numbers
648 ctx
.WriteSequenceNumber
= 0;
650 // all further data sent will be encrypted with the negotiated
651 // security parameters (now the current parameters)
652 if (ctx
is ClientContext
) {
653 ctx
.StartSwitchingSecurityParameters (true);
655 ctx
.EndSwitchingSecurityParameters (false);
659 public IAsyncResult
BeginSendRecord(HandshakeType handshakeType
, AsyncCallback callback
, object state
)
661 HandshakeMessage msg
= this.GetMessage(handshakeType
);
665 DebugHelper
.WriteLine(">>>> Write handshake record ({0}|{1})", context
.Protocol
, msg
.ContentType
);
667 SendRecordAsyncResult internalResult
= new SendRecordAsyncResult(callback
, state
, msg
);
669 this.BeginSendRecord(msg
.ContentType
, msg
.EncodeMessage(), new AsyncCallback(InternalSendRecordCallback
), internalResult
);
671 return internalResult
;
674 private void InternalSendRecordCallback(IAsyncResult ar
)
676 SendRecordAsyncResult internalResult
= ar
.AsyncState
as SendRecordAsyncResult
;
680 this.EndSendRecord(ar
);
683 internalResult
.Message
.Update();
685 // Reset message contents
686 internalResult
.Message
.Reset();
688 internalResult
.SetComplete();
692 internalResult
.SetComplete(ex
);
696 public IAsyncResult
BeginSendRecord(ContentType contentType
, byte[] recordData
, AsyncCallback callback
, object state
)
698 if (this.context
.ConnectionEnd
)
700 throw new TlsException(
701 AlertDescription
.InternalError
,
702 "The session is finished and it's no longer valid.");
705 byte[] record
= this.EncodeRecord(contentType
, recordData
);
707 return this.innerStream
.BeginWrite(record
, 0, record
.Length
, callback
, state
);
710 public void EndSendRecord(IAsyncResult asyncResult
)
712 if (asyncResult
is SendRecordAsyncResult
)
714 SendRecordAsyncResult internalResult
= asyncResult
as SendRecordAsyncResult
;
715 if (!internalResult
.IsCompleted
)
716 internalResult
.AsyncWaitHandle
.WaitOne();
717 if (internalResult
.CompletedWithError
)
718 throw internalResult
.AsyncException
;
722 this.innerStream
.EndWrite(asyncResult
);
726 public void SendRecord(ContentType contentType
, byte[] recordData
)
728 IAsyncResult ar
= this.BeginSendRecord(contentType
, recordData
, null, null);
730 this.EndSendRecord(ar
);
733 public byte[] EncodeRecord(ContentType contentType
, byte[] recordData
)
735 return this.EncodeRecord(
742 public byte[] EncodeRecord(
743 ContentType contentType
,
748 if (this.context
.ConnectionEnd
)
750 throw new TlsException(
751 AlertDescription
.InternalError
,
752 "The session is finished and it's no longer valid.");
755 TlsStream record
= new TlsStream();
757 int position
= offset
;
759 while (position
< ( offset
+ count
))
761 short fragmentLength
= 0;
764 if ((count
+ offset
- position
) > Context
.MAX_FRAGMENT_SIZE
)
766 fragmentLength
= Context
.MAX_FRAGMENT_SIZE
;
770 fragmentLength
= (short)(count
+ offset
- position
);
773 // Fill the fragment data
774 fragment
= new byte[fragmentLength
];
775 Buffer
.BlockCopy(recordData
, position
, fragment
, 0, fragmentLength
);
777 if ((this.Context
.Write
!= null) && (this.Context
.Write
.Cipher
!= null))
780 fragment
= this.encryptRecordFragment (contentType
, fragment
);
784 record
.Write((byte)contentType
);
785 record
.Write(this.context
.Protocol
);
786 record
.Write((short)fragment
.Length
);
787 record
.Write(fragment
);
789 DebugHelper
.WriteLine("Record data", fragment
);
791 // Update buffer position
792 position
+= fragmentLength
;
795 return record
.ToArray();
800 #region Cryptography Methods
802 private byte[] encryptRecordFragment(
803 ContentType contentType
,
808 // Calculate message MAC
809 if (this.Context
is ClientContext
)
811 mac
= this.context
.Write
.Cipher
.ComputeClientRecordMAC(contentType
, fragment
);
815 mac
= this.context
.Write
.Cipher
.ComputeServerRecordMAC (contentType
, fragment
);
818 DebugHelper
.WriteLine(">>>> Record MAC", mac
);
820 // Encrypt the message
821 byte[] ecr
= this.context
.Write
.Cipher
.EncryptRecord (fragment
, mac
);
823 // Update sequence number
824 this.context
.WriteSequenceNumber
++;
829 private byte[] decryptRecordFragment(
830 ContentType contentType
,
833 byte[] dcrFragment
= null;
834 byte[] dcrMAC
= null;
838 this.context
.Read
.Cipher
.DecryptRecord (fragment
, out dcrFragment
, out dcrMAC
);
842 if (this.context
is ServerContext
)
844 this.Context
.RecordProtocol
.SendAlert(AlertDescription
.DecryptionFailed
);
850 // Generate record MAC
853 if (this.Context
is ClientContext
)
855 mac
= this.context
.Read
.Cipher
.ComputeServerRecordMAC(contentType
, dcrFragment
);
859 mac
= this.context
.Read
.Cipher
.ComputeClientRecordMAC (contentType
, dcrFragment
);
862 DebugHelper
.WriteLine(">>>> Record MAC", mac
);
865 if (!Compare (mac
, dcrMAC
))
867 throw new TlsException(AlertDescription
.BadRecordMAC
, "Bad record MAC");
870 // Update sequence number
871 this.context
.ReadSequenceNumber
++;
876 private bool Compare (byte[] array1
, byte[] array2
)
879 return (array2
== null);
882 if (array1
.Length
!= array2
.Length
)
884 for (int i
= 0; i
< array1
.Length
; i
++) {
885 if (array1
[i
] != array2
[i
])
893 #region CipherSpecV2 processing
895 private void ProcessCipherSpecV2Buffer (SecurityProtocolType protocol
, byte[] buffer
)
897 TlsStream codes
= new TlsStream(buffer
);
899 string prefix
= (protocol
== SecurityProtocolType
.Ssl3
) ? "SSL_" : "TLS_";
901 while (codes
.Position
< codes
.Length
)
903 byte check
= codes
.ReadByte();
907 // SSL/TLS cipher spec
908 short code
= codes
.ReadInt16();
909 int index
= this.Context
.SupportedCiphers
.IndexOf(code
);
912 this.Context
.Negotiating
.Cipher
= this.Context
.SupportedCiphers
[index
];
918 byte[] tmp
= new byte[2];
919 codes
.Read(tmp
, 0, tmp
.Length
);
921 int tmpCode
= ((check
& 0xff) << 16) | ((tmp
[0] & 0xff) << 8) | (tmp
[1] & 0xff);
922 CipherSuite cipher
= this.MapV2CipherCode(prefix
, tmpCode
);
926 this.Context
.Negotiating
.Cipher
= cipher
;
932 if (this.Context
.Negotiating
== null)
934 throw new TlsException(AlertDescription
.InsuficientSecurity
, "Insuficient Security");
938 private CipherSuite
MapV2CipherCode(string prefix
, int code
)
945 // TLS_RC4_128_WITH_MD5
946 return this.Context
.SupportedCiphers
[prefix
+ "RSA_WITH_RC4_128_MD5"];
949 // TLS_RC4_128_EXPORT40_WITH_MD5
950 return this.Context
.SupportedCiphers
[prefix
+ "RSA_EXPORT_WITH_RC4_40_MD5"];
953 // TLS_RC2_CBC_128_CBC_WITH_MD5
954 return this.Context
.SupportedCiphers
[prefix
+ "RSA_EXPORT_WITH_RC2_CBC_40_MD5"];
957 // TLS_RC2_CBC_128_CBC_EXPORT40_WITH_MD5
958 return this.Context
.SupportedCiphers
[prefix
+ "RSA_EXPORT_WITH_RC2_CBC_40_MD5"];
961 // TLS_IDEA_128_CBC_WITH_MD5
965 // TLS_DES_64_CBC_WITH_MD5
969 // TLS_DES_192_EDE3_CBC_WITH_MD5