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