2010-03-03 Rodrigo Kumpera <rkumpera@novell.com>
[mono.git] / mcs / class / Mono.Security / Mono.Security.Protocol.Tls.Handshake.Client / TlsServerCertificate.cs
1 // Transport Security Layer (TLS)
2 // Copyright (c) 2003-2004 Carlos Guzman Alvarez
3 // Copyright (C) 2004, 2006-2010 Novell, Inc (http://www.novell.com)
4 //
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:
12 // 
13 // The above copyright notice and this permission notice shall be
14 // included in all copies or substantial portions of the Software.
15 // 
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.
23 //
24
25 using System;
26 using System.Net;
27 using System.Collections;
28 using System.Globalization;
29 using System.Text.RegularExpressions;
30 using System.Security.Cryptography;
31 using X509Cert = System.Security.Cryptography.X509Certificates;
32
33 using Mono.Security.X509;
34 using Mono.Security.X509.Extensions;
35
36 namespace Mono.Security.Protocol.Tls.Handshake.Client
37 {
38         internal class TlsServerCertificate : HandshakeMessage
39         {
40                 #region Fields
41
42                 private X509CertificateCollection certificates;
43                 
44                 #endregion
45
46                 #region Constructors
47
48                 public TlsServerCertificate(Context context, byte[] buffer) 
49                         : base(context, HandshakeType.Certificate, buffer)
50                 {
51                 }
52
53                 #endregion
54
55                 #region Methods
56
57                 public override void Update()
58                 {
59                         base.Update();
60                         this.Context.ServerSettings.Certificates = this.certificates;
61                         this.Context.ServerSettings.UpdateCertificateRSA();
62                 }
63
64                 #endregion
65
66                 #region Protected Methods
67
68                 protected override void ProcessAsSsl3()
69                 {
70                         this.ProcessAsTls1();
71                 }
72
73                 protected override void ProcessAsTls1()
74                 {
75                         this.certificates = new X509CertificateCollection();
76                         
77                         int readed      = 0;
78                         int length      = this.ReadInt24();
79
80                         while (readed < length)
81                         {
82                                 // Read certificate length
83                                 int certLength = ReadInt24();
84
85                                 // Increment readed
86                                 readed += 3;
87
88                                 if (certLength > 0)
89                                 {
90                                         // Read certificate data
91                                         byte[] buffer = this.ReadBytes(certLength);
92
93                                         // Create a new X509 Certificate
94                                         X509Certificate certificate = new X509Certificate(buffer);
95                                         certificates.Add(certificate);
96
97                                         readed += certLength;
98
99                                         DebugHelper.WriteLine(
100                                                 String.Format("Server Certificate {0}", certificates.Count),
101                                                 buffer);
102                                 }
103                         }
104
105                         this.validateCertificates(certificates);
106                 }
107
108                 #endregion
109
110                 #region Private Methods
111
112                 // Note: this method only works for RSA certificates
113                 // DH certificates requires some changes - does anyone use one ?
114                 private bool checkCertificateUsage (X509Certificate cert) 
115                 {
116                         ClientContext context = (ClientContext)this.Context;
117
118                         // certificate extensions are required for this
119                         // we "must" accept older certificates without proofs
120                         if (cert.Version < 3)
121                                 return true;
122
123                         KeyUsages ku = KeyUsages.none;
124                         switch (context.Negotiating.Cipher.ExchangeAlgorithmType) 
125                         {
126                                 case ExchangeAlgorithmType.RsaSign:
127                                         ku = KeyUsages.digitalSignature;
128                                         break;
129                                 case ExchangeAlgorithmType.RsaKeyX:
130                                         ku = KeyUsages.keyEncipherment;
131                                         break;
132                                 case ExchangeAlgorithmType.DiffieHellman:
133                                         ku = KeyUsages.keyAgreement;
134                                         break;
135                                 case ExchangeAlgorithmType.Fortezza:
136                                         return false; // unsupported certificate type
137                         }
138
139                         KeyUsageExtension kux = null;
140                         ExtendedKeyUsageExtension eku = null;
141
142                         X509Extension xtn = cert.Extensions ["2.5.29.15"];
143                         if (xtn != null)
144                                 kux = new KeyUsageExtension (xtn);
145
146                         xtn = cert.Extensions ["2.5.29.37"];
147                         if (xtn != null)
148                                 eku = new ExtendedKeyUsageExtension (xtn);
149
150                         if ((kux != null) && (eku != null)) 
151                         {
152                                 // RFC3280 states that when both KeyUsageExtension and 
153                                 // ExtendedKeyUsageExtension are present then BOTH should
154                                 // be valid
155                                 if (!kux.Support (ku))
156                                         return false;
157                                 return (eku.KeyPurpose.Contains ("1.3.6.1.5.5.7.3.1") ||
158                                         eku.KeyPurpose.Contains ("2.16.840.1.113730.4.1"));
159                         }
160                         else if (kux != null) 
161                         {
162                                 return kux.Support (ku);
163                         }
164                         else if (eku != null) 
165                         {
166                                 // Server Authentication (1.3.6.1.5.5.7.3.1) or
167                                 // Netscape Server Gated Crypto (2.16.840.1.113730.4)
168                                 return (eku.KeyPurpose.Contains ("1.3.6.1.5.5.7.3.1") ||
169                                         eku.KeyPurpose.Contains ("2.16.840.1.113730.4.1"));
170                         }
171
172                         // last chance - try with older (deprecated) Netscape extensions
173                         xtn = cert.Extensions ["2.16.840.1.113730.1.1"];
174                         if (xtn != null) 
175                         {
176                                 NetscapeCertTypeExtension ct = new NetscapeCertTypeExtension (xtn);
177                                 return ct.Support (NetscapeCertTypeExtension.CertTypes.SslServer);
178                         }
179
180                         // if the CN=host (checked later) then we assume this is meant for SSL/TLS
181                         // e.g. the new smtp.gmail.com certificate
182                         return true;
183                 }
184
185                 
186                 static private void VerifyOSX (X509CertificateCollection certificates)
187                 {
188                         
189                 }
190                 
191                 private void validateCertificates(X509CertificateCollection certificates)
192                 {
193                         ClientContext           context                 = (ClientContext)this.Context;
194                         AlertDescription        description             = AlertDescription.BadCertificate;
195
196 #if NET_2_0
197                         if (context.SslStream.HaveRemoteValidation2Callback) {
198                                 if (context.SslStream.RaiseServerCertificateValidation2 (certificates))
199                                         return;
200                                 // Give a chance to the 1.x ICertificatePolicy callback
201                         }
202 #endif
203                         // the leaf is the web server certificate
204                         X509Certificate leaf = certificates [0];
205                         X509Cert.X509Certificate cert = new X509Cert.X509Certificate (leaf.RawData);
206
207                         ArrayList errors = new ArrayList();
208
209                         // SSL specific check - not all certificates can be 
210                         // used to server-side SSL some rules applies after 
211                         // all ;-)
212                         if (!checkCertificateUsage (leaf)) 
213                         {
214                                 // WinError.h CERT_E_PURPOSE 0x800B0106
215                                 errors.Add ((int)-2146762490);
216                         }
217
218                         // SSL specific check - does the certificate match 
219                         // the host ?
220                         if (!checkServerIdentity (leaf))
221                         {
222                                 // WinError.h CERT_E_CN_NO_MATCH 0x800B010F
223                                 errors.Add ((int)-2146762481);
224                         }
225
226                         // Note: building and verifying a chain can take much time
227                         // so we do it last (letting simple things fails first)
228
229                         // Note: In TLS the certificates MUST be in order (and
230                         // optionally include the root certificate) so we're not
231                         // building the chain using LoadCertificate (it's faster)
232
233                         // Note: IIS doesn't seem to send the whole certificate chain
234                         // but only the server certificate :-( it's assuming that you
235                         // already have this chain installed on your computer. duh!
236                         // http://groups.google.ca/groups?q=IIS+server+certificate+chain&hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=85058s%24avd%241%40nnrp1.deja.com&rnum=3
237
238                         // we must remove the leaf certificate from the chain
239                         X509CertificateCollection chain = new X509CertificateCollection (certificates);
240                         chain.Remove (leaf);
241                         X509Chain verify = new X509Chain (chain);
242
243                         bool result = false;
244
245                         try
246                         {
247                                 result = verify.Build (leaf);
248                         }
249                         catch (Exception)
250                         {
251                                 result = false;
252                         }
253
254                         // Attempt to use OSX certificates
255                         //
256                         // Ideally we should return the SecTrustResult
257 #if !MONOTOUCH
258                         if (System.IO.File.Exists (OSX509Certificates.SecurityLibrary)){
259 #endif
260                                 OSX509Certificates.SecTrustResult trustResult =  OSX509Certificates.TrustEvaluateSsl (certificates);
261
262                                 // We could use the other values of trustResult to pass this extra information to the .NET 2 callback
263                                 // for values like SecTrustResult.Confirm
264                                 result = (trustResult == OSX509Certificates.SecTrustResult.Proceed ||
265                                           trustResult == OSX509Certificates.SecTrustResult.Unspecified);
266 #if !MONOTOUCH
267                         }
268 #endif
269                         
270                         if (!result) 
271                         {
272                                 switch (verify.Status) 
273                                 {
274                                         case X509ChainStatusFlags.InvalidBasicConstraints:
275                                                 // WinError.h TRUST_E_BASIC_CONSTRAINTS 0x80096019
276                                                 errors.Add ((int)-2146869223);
277                                                 break;
278                                         
279                                         case X509ChainStatusFlags.NotSignatureValid:
280                                                 // WinError.h TRUST_E_BAD_DIGEST 0x80096010
281                                                 errors.Add ((int)-2146869232);
282                                                 break;
283                                         
284                                         case X509ChainStatusFlags.NotTimeNested:
285                                                 // WinError.h CERT_E_VALIDITYPERIODNESTING 0x800B0102
286                                                 errors.Add ((int)-2146762494);
287                                                 break;
288                                         
289                                         case X509ChainStatusFlags.NotTimeValid:
290                                                 // WinError.h CERT_E_EXPIRED 0x800B0101
291                                                 description = AlertDescription.CertificateExpired;
292                                                 errors.Add ((int)-2146762495);
293                                                 break;
294                                         
295                                         case X509ChainStatusFlags.PartialChain:
296                                                 // WinError.h CERT_E_CHAINING 0x800B010A
297                                                 description = AlertDescription.UnknownCA;
298                                                 errors.Add ((int)-2146762486);
299                                                 break;
300                                         
301                                         case X509ChainStatusFlags.UntrustedRoot:
302                                                 // WinError.h CERT_E_UNTRUSTEDROOT 0x800B0109
303                                                 description = AlertDescription.UnknownCA;
304                                                 errors.Add ((int)-2146762487);
305                                                 break;
306                                         
307                                         default:
308                                                 // unknown error
309                                                 description = AlertDescription.CertificateUnknown;
310                                                 errors.Add ((int)verify.Status);
311                                                 break;
312                                 }
313                         }
314
315                         int[] certificateErrors = (int[])errors.ToArray(typeof(int));
316
317                         if (!context.SslStream.RaiseServerCertificateValidation(
318                                 cert, 
319                                 certificateErrors))
320                         {
321                                 throw new TlsException(
322                                         description,
323                                         "Invalid certificate received from server.");
324                         }
325                 }
326
327                 // RFC2818 - HTTP Over TLS, Section 3.1
328                 // http://www.ietf.org/rfc/rfc2818.txt
329                 // 
330                 // 1.   if present MUST use subjectAltName dNSName as identity
331                 // 1.1.         if multiples entries a match of any one is acceptable
332                 // 1.2.         wildcard * is acceptable
333                 // 2.   URI may be an IP address -> subjectAltName.iPAddress
334                 // 2.1.         exact match is required
335                 // 3.   Use of the most specific Common Name (CN=) in the Subject
336                 // 3.1          Existing practice but DEPRECATED
337                 private bool checkServerIdentity (X509Certificate cert) 
338                 {
339                         ClientContext context = (ClientContext)this.Context;
340
341                         string targetHost = context.ClientSettings.TargetHost;
342
343                         X509Extension ext = cert.Extensions ["2.5.29.17"];
344                         // 1. subjectAltName
345                         if (ext != null) 
346                         {
347                                 SubjectAltNameExtension subjectAltName = new SubjectAltNameExtension (ext);
348                                 // 1.1 - multiple dNSName
349                                 foreach (string dns in subjectAltName.DNSNames) 
350                                 {
351                                         // 1.2 TODO - wildcard support
352                                         if (Match (targetHost, dns))
353                                                 return true;
354                                 }
355                                 // 2. ipAddress
356                                 foreach (string ip in subjectAltName.IPAddresses) 
357                                 {
358                                         // 2.1. Exact match required
359                                         if (ip == targetHost)
360                                                 return true;
361                                 }
362                         }
363                         // 3. Common Name (CN=)
364                         return checkDomainName (cert.SubjectName);
365                 }
366
367                 private bool checkDomainName(string subjectName)
368                 {
369                         ClientContext context = (ClientContext)this.Context;
370
371                         string  domainName = String.Empty;
372                         Regex search = new Regex(@"CN\s*=\s*([^,]*)");
373
374                         MatchCollection elements = search.Matches(subjectName);
375
376                         if (elements.Count == 1)
377                         {
378                                 if (elements[0].Success)
379                                 {
380                                         domainName = elements[0].Groups[1].Value.ToString();
381                                 }
382                         }
383
384                         return Match (context.ClientSettings.TargetHost, domainName);
385                 }
386
387                 // ensure the pattern is valid wrt to RFC2595 and RFC2818
388                 // http://www.ietf.org/rfc/rfc2595.txt
389                 // http://www.ietf.org/rfc/rfc2818.txt
390                 static bool Match (string hostname, string pattern)
391                 {
392                         // check if this is a pattern
393                         int index = pattern.IndexOf ('*');
394                         if (index == -1) {
395                                 // not a pattern, do a direct case-insensitive comparison
396                                 return (String.Compare (hostname, pattern, true, CultureInfo.InvariantCulture) == 0);
397                         }
398
399                         // check pattern validity
400                         // A "*" wildcard character MAY be used as the left-most name component in the certificate.
401
402                         // unless this is the last char (valid)
403                         if (index != pattern.Length - 1) {
404                                 // then the next char must be a dot .'.
405                                 if (pattern [index + 1] != '.')
406                                         return false;
407                         }
408
409                         // only one (A) wildcard is supported
410                         int i2 = pattern.IndexOf ('*', index + 1);
411                         if (i2 != -1)
412                                 return false;
413
414                         // match the end of the pattern
415                         string end = pattern.Substring (index + 1);
416                         int length = hostname.Length - end.Length;
417                         // no point to check a pattern that is longer than the hostname
418                         if (length <= 0)
419                                 return false;
420
421                         if (String.Compare (hostname, length, end, 0, end.Length, true, CultureInfo.InvariantCulture) != 0)
422                                 return false;
423
424                         // special case, we start with the wildcard
425                         if (index == 0) {
426                                 // ensure we hostname non-matched part (start) doesn't contain a dot
427                                 int i3 = hostname.IndexOf ('.');
428                                 return ((i3 == -1) || (i3 >= (hostname.Length - end.Length)));
429                         }
430
431                         // match the start of the pattern
432                         string start = pattern.Substring (0, index);
433                         return (String.Compare (hostname, 0, start, 0, start.Length, true, CultureInfo.InvariantCulture) == 0);
434                 }
435
436                 #endregion
437         }
438 }