Merge pull request #629 from pruiz/syswebrouting-fixes2
[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                 private void validateCertificates(X509CertificateCollection certificates)
186                 {
187                         ClientContext           context                 = (ClientContext)this.Context;
188                         AlertDescription        description             = AlertDescription.BadCertificate;
189
190 #if INSIDE_SYSTEM
191                         // This helps the linker to remove a lot of validation code that will never be used since 
192                         // System.dll will, for OSX and iOS, uses the operating system X.509 certificate validations
193                         RemoteValidation (context, description);
194 #else
195                         if (context.SslStream.HaveRemoteValidation2Callback)
196                                 RemoteValidation (context, description);
197                         else 
198                                 LocalValidation (context, description);
199 #endif
200                 }
201
202                 void RemoteValidation (ClientContext context, AlertDescription description)
203                 {
204                         ValidationResult res = context.SslStream.RaiseServerCertificateValidation2 (certificates);
205                         if (res.Trusted)
206                                 return;
207
208                         long error = res.ErrorCode;
209                         switch (error) {
210                         case 0x800B0101:
211                                 description = AlertDescription.CertificateExpired;
212                                 break;
213                         case 0x800B010A:
214                                 description = AlertDescription.UnknownCA;
215                                 break;
216                         case 0x800B0109:
217                                 description = AlertDescription.UnknownCA;
218                                 break;
219                         default:
220                                 description = AlertDescription.CertificateUnknown;
221                                 break;
222                         }
223                         string err = String.Format ("Invalid certificate received from server. Error code: 0x{0:x}", error);
224                         throw new TlsException (description, err);
225                 }
226
227                 void LocalValidation (ClientContext context, AlertDescription description)
228                 {
229                         // the leaf is the web server certificate
230                         X509Certificate leaf = certificates [0];
231                         X509Cert.X509Certificate cert = new X509Cert.X509Certificate (leaf.RawData);
232
233                         ArrayList errors = new ArrayList();
234
235                         // SSL specific check - not all certificates can be 
236                         // used to server-side SSL some rules applies after 
237                         // all ;-)
238                         if (!checkCertificateUsage (leaf)) 
239                         {
240                                 // WinError.h CERT_E_PURPOSE 0x800B0106
241                                 errors.Add ((int)-2146762490);
242                         }
243
244                         // SSL specific check - does the certificate match 
245                         // the host ?
246                         if (!checkServerIdentity (leaf))
247                         {
248                                 // WinError.h CERT_E_CN_NO_MATCH 0x800B010F
249                                 errors.Add ((int)-2146762481);
250                         }
251
252                         // Note: building and verifying a chain can take much time
253                         // so we do it last (letting simple things fails first)
254
255                         // Note: In TLS the certificates MUST be in order (and
256                         // optionally include the root certificate) so we're not
257                         // building the chain using LoadCertificate (it's faster)
258
259                         // Note: IIS doesn't seem to send the whole certificate chain
260                         // but only the server certificate :-( it's assuming that you
261                         // already have this chain installed on your computer. duh!
262                         // 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
263
264                         // we must remove the leaf certificate from the chain
265                         X509CertificateCollection chain = new X509CertificateCollection (certificates);
266                         chain.Remove (leaf);
267                         X509Chain verify = new X509Chain (chain);
268
269                         bool result = false;
270
271                         try
272                         {
273                                 result = verify.Build (leaf);
274                         }
275                         catch (Exception)
276                         {
277                                 result = false;
278                         }
279
280                         if (!result) 
281                         {
282                                 switch (verify.Status) 
283                                 {
284                                         case X509ChainStatusFlags.InvalidBasicConstraints:
285                                                 // WinError.h TRUST_E_BASIC_CONSTRAINTS 0x80096019
286                                                 errors.Add ((int)-2146869223);
287                                                 break;
288                                         
289                                         case X509ChainStatusFlags.NotSignatureValid:
290                                                 // WinError.h TRUST_E_BAD_DIGEST 0x80096010
291                                                 errors.Add ((int)-2146869232);
292                                                 break;
293                                         
294                                         case X509ChainStatusFlags.NotTimeNested:
295                                                 // WinError.h CERT_E_VALIDITYPERIODNESTING 0x800B0102
296                                                 errors.Add ((int)-2146762494);
297                                                 break;
298                                         
299                                         case X509ChainStatusFlags.NotTimeValid:
300                                                 // WinError.h CERT_E_EXPIRED 0x800B0101
301                                                 description = AlertDescription.CertificateExpired;
302                                                 errors.Add ((int)-2146762495);
303                                                 break;
304                                         
305                                         case X509ChainStatusFlags.PartialChain:
306                                                 // WinError.h CERT_E_CHAINING 0x800B010A
307                                                 description = AlertDescription.UnknownCA;
308                                                 errors.Add ((int)-2146762486);
309                                                 break;
310                                         
311                                         case X509ChainStatusFlags.UntrustedRoot:
312                                                 // WinError.h CERT_E_UNTRUSTEDROOT 0x800B0109
313                                                 description = AlertDescription.UnknownCA;
314                                                 errors.Add ((int)-2146762487);
315                                                 break;
316                                         
317                                         default:
318                                                 // unknown error
319                                                 description = AlertDescription.CertificateUnknown;
320                                                 errors.Add ((int)verify.Status);
321                                                 break;
322                                 }
323                         }
324
325                         int[] certificateErrors = (int[])errors.ToArray(typeof(int));
326
327                         if (!context.SslStream.RaiseServerCertificateValidation(
328                                 cert, 
329                                 certificateErrors))
330                         {
331                                 throw new TlsException(
332                                         description,
333                                         "Invalid certificate received from server.");
334                         }
335                 }
336
337                 // RFC2818 - HTTP Over TLS, Section 3.1
338                 // http://www.ietf.org/rfc/rfc2818.txt
339                 // 
340                 // 1.   if present MUST use subjectAltName dNSName as identity
341                 // 1.1.         if multiples entries a match of any one is acceptable
342                 // 1.2.         wildcard * is acceptable
343                 // 2.   URI may be an IP address -> subjectAltName.iPAddress
344                 // 2.1.         exact match is required
345                 // 3.   Use of the most specific Common Name (CN=) in the Subject
346                 // 3.1          Existing practice but DEPRECATED
347                 private bool checkServerIdentity (X509Certificate cert) 
348                 {
349                         ClientContext context = (ClientContext)this.Context;
350
351                         string targetHost = context.ClientSettings.TargetHost;
352
353                         X509Extension ext = cert.Extensions ["2.5.29.17"];
354                         // 1. subjectAltName
355                         if (ext != null) 
356                         {
357                                 SubjectAltNameExtension subjectAltName = new SubjectAltNameExtension (ext);
358                                 // 1.1 - multiple dNSName
359                                 foreach (string dns in subjectAltName.DNSNames) 
360                                 {
361                                         // 1.2 TODO - wildcard support
362                                         if (Match (targetHost, dns))
363                                                 return true;
364                                 }
365                                 // 2. ipAddress
366                                 foreach (string ip in subjectAltName.IPAddresses) 
367                                 {
368                                         // 2.1. Exact match required
369                                         if (ip == targetHost)
370                                                 return true;
371                                 }
372                         }
373                         // 3. Common Name (CN=)
374                         return checkDomainName (cert.SubjectName);
375                 }
376
377                 private bool checkDomainName(string subjectName)
378                 {
379                         ClientContext context = (ClientContext)this.Context;
380
381                         string  domainName = String.Empty;
382                         Regex search = new Regex(@"CN\s*=\s*([^,]*)");
383
384                         MatchCollection elements = search.Matches(subjectName);
385
386                         if (elements.Count == 1)
387                         {
388                                 if (elements[0].Success)
389                                 {
390                                         domainName = elements[0].Groups[1].Value.ToString();
391                                 }
392                         }
393
394                         return Match (context.ClientSettings.TargetHost, domainName);
395                 }
396
397                 // ensure the pattern is valid wrt to RFC2595 and RFC2818
398                 // http://www.ietf.org/rfc/rfc2595.txt
399                 // http://www.ietf.org/rfc/rfc2818.txt
400                 static bool Match (string hostname, string pattern)
401                 {
402                         // check if this is a pattern
403                         int index = pattern.IndexOf ('*');
404                         if (index == -1) {
405                                 // not a pattern, do a direct case-insensitive comparison
406                                 return (String.Compare (hostname, pattern, true, CultureInfo.InvariantCulture) == 0);
407                         }
408
409                         // check pattern validity
410                         // A "*" wildcard character MAY be used as the left-most name component in the certificate.
411
412                         // unless this is the last char (valid)
413                         if (index != pattern.Length - 1) {
414                                 // then the next char must be a dot .'.
415                                 if (pattern [index + 1] != '.')
416                                         return false;
417                         }
418
419                         // only one (A) wildcard is supported
420                         int i2 = pattern.IndexOf ('*', index + 1);
421                         if (i2 != -1)
422                                 return false;
423
424                         // match the end of the pattern
425                         string end = pattern.Substring (index + 1);
426                         int length = hostname.Length - end.Length;
427                         // no point to check a pattern that is longer than the hostname
428                         if (length <= 0)
429                                 return false;
430
431                         if (String.Compare (hostname, length, end, 0, end.Length, true, CultureInfo.InvariantCulture) != 0)
432                                 return false;
433
434                         // special case, we start with the wildcard
435                         if (index == 0) {
436                                 // ensure we hostname non-matched part (start) doesn't contain a dot
437                                 int i3 = hostname.IndexOf ('.');
438                                 return ((i3 == -1) || (i3 >= (hostname.Length - end.Length)));
439                         }
440
441                         // match the start of the pattern
442                         string start = pattern.Substring (0, index);
443                         return (String.Compare (hostname, 0, start, 0, start.Length, true, CultureInfo.InvariantCulture) == 0);
444                 }
445
446                 #endregion
447         }
448 }