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