[Mono.Security]: Add 'MonoTlsSettings.TrustAnchors' and 'CertificateValidationHelper...
[mono.git] / mcs / class / System / Mono.Net.Security / SystemCertificateValidator.cs
1 #if SECURITY_DEP
2
3 #if MONO_SECURITY_ALIAS
4 extern alias MonoSecurity;
5 #endif
6 #if MONO_X509_ALIAS
7 extern alias PrebuiltSystem;
8 #endif
9
10 #if MONO_SECURITY_ALIAS
11 using MonoSecurity::Mono.Security.Interface;
12 using MSX = MonoSecurity::Mono.Security.X509;
13 using MonoSecurity::Mono.Security.X509.Extensions;
14 #else
15 using Mono.Security.Interface;
16 using MSX = Mono.Security.X509;
17 using Mono.Security.X509.Extensions;
18 #endif
19 #if MONO_X509_ALIAS
20 using XX509CertificateCollection = PrebuiltSystem::System.Security.Cryptography.X509Certificates.X509CertificateCollection;
21 #else
22 using XX509CertificateCollection = System.Security.Cryptography.X509Certificates.X509CertificateCollection;
23 #endif
24
25 using System;
26 using System.Net;
27 using System.Threading;
28 using System.Collections;
29 using System.Collections.Generic;
30 using System.Collections.Specialized;
31 using System.Configuration;
32 using System.Net.Configuration;
33 using System.Text.RegularExpressions;
34 using System.Security.Cryptography.X509Certificates;
35
36 using System.Globalization;
37 using System.Net.Security;
38 using System.Diagnostics;
39
40 namespace Mono.Net.Security
41 {
42         internal class SystemCertificateValidator
43         {
44                 static bool is_macosx;
45                 static bool is_mobile;
46 #if !MOBILE
47                 static X509RevocationMode revocation_mode;
48 #endif
49
50                 static SystemCertificateValidator ()
51                 {
52 #if MONOTOUCH
53                         is_macosx = true;
54                         is_mobile = true;
55 #elif MONODROID
56                         is_macosx = false;
57                         is_mobile = true;
58 #else
59                         is_macosx = System.IO.File.Exists (OSX509Certificates.SecurityLibrary);
60                         is_mobile = false;
61 #endif
62
63 #if !MOBILE
64                         revocation_mode = X509RevocationMode.NoCheck;
65                         try {
66                                 string str = Environment.GetEnvironmentVariable ("MONO_X509_REVOCATION_MODE");
67                                 if (String.IsNullOrEmpty (str))
68                                         return;
69                                 revocation_mode = (X509RevocationMode)Enum.Parse (typeof(X509RevocationMode), str, true);
70                         } catch {
71                         }
72 #endif
73                 }
74
75                 public virtual X509Chain ComputeX509Chain (XX509CertificateCollection certs, ref SslPolicyErrors errors, ref int status11)
76                 {
77 #if MOBILE
78                         return null;
79 #else
80                         if (is_macosx)
81                                 return null;
82
83                         var chain = new X509Chain ();
84                         chain.ChainPolicy = new X509ChainPolicy ();
85
86                         chain.ChainPolicy.RevocationMode = revocation_mode;
87
88                         for (int i = 1; i < certs.Count; i++) {
89                                 chain.ChainPolicy.ExtraStore.Add (certs [i]);
90                         }
91
92                         var leaf = (X509Certificate2)certs [0];
93
94                         try {
95                                 if (!chain.Build (leaf))
96                                         errors |= GetErrorsFromChain (chain);
97                         } catch (Exception e) {
98                                 Console.Error.WriteLine ("ERROR building certificate chain: {0}", e);
99                                 Console.Error.WriteLine ("Please, report this problem to the Mono team");
100                                 errors |= SslPolicyErrors.RemoteCertificateChainErrors;
101                         }
102
103                         status11 = GetStatusFromChain (chain);
104
105                         return chain;
106 #endif
107                 }
108
109                 public virtual void CheckUsage (XX509CertificateCollection certs, string host, ref SslPolicyErrors errors, ref int status11)
110                 {
111 #if !MONOTOUCH
112                         var leaf = (X509Certificate2)certs[0];
113                         // for OSX and iOS we're using the native API to check for the SSL server policy and host names
114                         if (!is_macosx) {
115                                 if (!CheckCertificateUsage (leaf)) {
116                                         errors |= SslPolicyErrors.RemoteCertificateChainErrors;
117                                         status11 = -2146762490; //CERT_E_PURPOSE 0x800B0106
118                                 }
119
120                                 if (host != null && !CheckServerIdentity (leaf, host)) {
121                                         errors |= SslPolicyErrors.RemoteCertificateNameMismatch;
122                                         status11 = -2146762481; // CERT_E_CN_NO_MATCH 0x800B010F
123                                 }
124                         }
125 #endif
126                 }
127
128                 public virtual bool EvaluateSystem (XX509CertificateCollection certs, XX509CertificateCollection anchors, string host, X509Chain chain, ref SslPolicyErrors errors, ref int status11)
129                 {
130                         var leaf = certs [0];
131                         var result = false;
132
133 #if MONODROID
134                         result = AndroidPlatform.TrustEvaluateSsl (certs);
135                         if (result) {
136                                 // chain.Build() + GetErrorsFromChain() (above) will ALWAYS fail on
137                                 // Android (there are no mozroots or preinstalled root certificates),
138                                 // thus `errors` will ALWAYS have RemoteCertificateChainErrors.
139                                 // Android just verified the chain; clear RemoteCertificateChainErrors.
140                                 errors  &= ~SslPolicyErrors.RemoteCertificateChainErrors;
141                         }
142 #else
143                         if (is_macosx) {
144                                 // Attempt to use OSX certificates
145                                 // Ideally we should return the SecTrustResult
146                                 OSX509Certificates.SecTrustResult trustResult = OSX509Certificates.SecTrustResult.Deny;
147                                 try {
148                                         trustResult = OSX509Certificates.TrustEvaluateSsl (certs, anchors, host);
149                                         // We could use the other values of trustResult to pass this extra information
150                                         // to the .NET 2 callback for values like SecTrustResult.Confirm
151                                         result = (trustResult == OSX509Certificates.SecTrustResult.Proceed ||
152                                                 trustResult == OSX509Certificates.SecTrustResult.Unspecified);
153                                 } catch {
154                                         // Ignore
155                                 }
156
157                                 if (result) {
158                                         // TrustEvaluateSsl was successful so there's no trust error
159                                         // IOW we discard our own chain (since we trust OSX one instead)
160                                         errors = 0;
161                                 } else {
162                                         // callback and DefaultCertificatePolicy needs this since 'result' is not specified
163                                         status11 = (int)trustResult;
164                                         errors |= SslPolicyErrors.RemoteCertificateChainErrors;
165                                 }
166                         }
167 #endif
168
169                         return result;
170                 }
171
172 #if !MOBILE
173                 static int GetStatusFromChain (X509Chain chain)
174                 {
175                         long result = 0;
176                         foreach (var status in chain.ChainStatus) {
177                                 X509ChainStatusFlags flags = status.Status;
178                                 if (flags == X509ChainStatusFlags.NoError)
179                                         continue;
180
181                                 // CERT_E_EXPIRED
182                                 if ((flags & X509ChainStatusFlags.NotTimeValid) != 0)
183                                         result = 0x800B0101;
184                                 // CERT_E_VALIDITYPERIODNESTING
185                                 else if ((flags & X509ChainStatusFlags.NotTimeNested) != 0)
186                                         result = 0x800B0102;
187                                 // CERT_E_REVOKED
188                                 else if ((flags & X509ChainStatusFlags.Revoked) != 0)
189                                         result = 0x800B010C;
190                                 // TRUST_E_CERT_SIGNATURE
191                                 else if ((flags & X509ChainStatusFlags.NotSignatureValid) != 0)
192                                         result = 0x80096004;
193                                 // CERT_E_WRONG_USAGE
194                                 else if ((flags & X509ChainStatusFlags.NotValidForUsage) != 0)
195                                         result = 0x800B0110;
196                                 // CERT_E_UNTRUSTEDROOT
197                                 else if ((flags & X509ChainStatusFlags.UntrustedRoot) != 0)
198                                         result = 0x800B0109;
199                                 // CRYPT_E_NO_REVOCATION_CHECK
200                                 else if ((flags & X509ChainStatusFlags.RevocationStatusUnknown) != 0)
201                                         result = 0x80092012;
202                                 // CERT_E_CHAINING
203                                 else if ((flags & X509ChainStatusFlags.Cyclic) != 0)
204                                         result = 0x800B010A;
205                                 // TRUST_E_FAIL - generic
206                                 else if ((flags & X509ChainStatusFlags.InvalidExtension) != 0)
207                                         result = 0x800B010B;
208                                 // CERT_E_UNTRUSTEDROOT
209                                 else if ((flags & X509ChainStatusFlags.InvalidPolicyConstraints) != 0)
210                                         result = 0x800B010D;
211                                 // TRUST_E_BASIC_CONSTRAINTS
212                                 else if ((flags & X509ChainStatusFlags.InvalidBasicConstraints) != 0)
213                                         result = 0x80096019;
214                                 // CERT_E_INVALID_NAME
215                                 else if ((flags & X509ChainStatusFlags.InvalidNameConstraints) != 0)
216                                         result = 0x800B0114;
217                                 // CERT_E_INVALID_NAME
218                                 else if ((flags & X509ChainStatusFlags.HasNotSupportedNameConstraint) != 0)
219                                         result = 0x800B0114;
220                                 // CERT_E_INVALID_NAME
221                                 else if ((flags & X509ChainStatusFlags.HasNotDefinedNameConstraint) != 0)
222                                         result = 0x800B0114;
223                                 // CERT_E_INVALID_NAME
224                                 else if ((flags & X509ChainStatusFlags.HasNotPermittedNameConstraint) != 0)
225                                         result = 0x800B0114;
226                                 // CERT_E_INVALID_NAME
227                                 else if ((flags & X509ChainStatusFlags.HasExcludedNameConstraint) != 0)
228                                         result = 0x800B0114;
229                                 // CERT_E_CHAINING
230                                 else if ((flags & X509ChainStatusFlags.PartialChain) != 0)
231                                         result = 0x800B010A;
232                                 // CERT_E_EXPIRED
233                                 else if ((flags & X509ChainStatusFlags.CtlNotTimeValid) != 0)
234                                         result = 0x800B0101;
235                                 // TRUST_E_CERT_SIGNATURE
236                                 else if ((flags & X509ChainStatusFlags.CtlNotSignatureValid) != 0)
237                                         result = 0x80096004;
238                                 // CERT_E_WRONG_USAGE
239                                 else if ((flags & X509ChainStatusFlags.CtlNotValidForUsage) != 0)
240                                         result = 0x800B0110;
241                                 // CRYPT_E_NO_REVOCATION_CHECK
242                                 else if ((flags & X509ChainStatusFlags.OfflineRevocation) != 0)
243                                         result = 0x80092012;
244                                 // CERT_E_ISSUERCHAINING
245                                 else if ((flags & X509ChainStatusFlags.NoIssuanceChainPolicy) != 0)
246                                         result = 0x800B0107;
247                                 else
248                                         result = 0x800B010B; // TRUST_E_FAIL - generic
249
250                                 break; // Exit the loop on the first error
251                         }
252                         return (int)result;
253                 }
254
255                 static SslPolicyErrors GetErrorsFromChain (X509Chain chain)
256                 {
257                         SslPolicyErrors errors = SslPolicyErrors.None;
258                         foreach (var status in chain.ChainStatus) {
259                                 if (status.Status == X509ChainStatusFlags.NoError)
260                                         continue;
261                                 errors |= SslPolicyErrors.RemoteCertificateChainErrors;
262                                 break;
263                         }
264                         return errors;
265                 }
266 #endif
267
268 #if !MONOTOUCH
269                 static X509KeyUsageFlags s_flags = X509KeyUsageFlags.DigitalSignature |
270                         X509KeyUsageFlags.KeyAgreement |
271                         X509KeyUsageFlags.KeyEncipherment;
272                 // Adapted to System 2.0+ from TlsServerCertificate.cs
273                 //------------------------------
274                 // Note: this method only works for RSA certificates
275                 // DH certificates requires some changes - does anyone use one ?
276                 static bool CheckCertificateUsage (X509Certificate2 cert)
277                 {
278                         try {
279                                 // certificate extensions are required for this
280                                 // we "must" accept older certificates without proofs
281                                 if (cert.Version < 3)
282                                         return true;
283
284                                 X509KeyUsageExtension kux = (cert.Extensions ["2.5.29.15"] as X509KeyUsageExtension);
285                                 X509EnhancedKeyUsageExtension eku = (cert.Extensions ["2.5.29.37"] as X509EnhancedKeyUsageExtension);
286                                 if (kux != null && eku != null) {
287                                         // RFC3280 states that when both KeyUsageExtension and 
288                                         // ExtendedKeyUsageExtension are present then BOTH should
289                                         // be valid
290                                         if ((kux.KeyUsages & s_flags) == 0)
291                                                 return false;
292                                         return eku.EnhancedKeyUsages ["1.3.6.1.5.5.7.3.1"] != null ||
293                                                 eku.EnhancedKeyUsages ["2.16.840.1.113730.4.1"] != null;
294                                 } else if (kux != null) {
295                                         return ((kux.KeyUsages & s_flags) != 0);
296                                 } else if (eku != null) {
297                                         // Server Authentication (1.3.6.1.5.5.7.3.1) or
298                                         // Netscape Server Gated Crypto (2.16.840.1.113730.4)
299                                         return eku.EnhancedKeyUsages ["1.3.6.1.5.5.7.3.1"] != null ||
300                                                 eku.EnhancedKeyUsages ["2.16.840.1.113730.4.1"] != null;
301                                 }
302
303                                 // last chance - try with older (deprecated) Netscape extensions
304                                 X509Extension ext = cert.Extensions ["2.16.840.1.113730.1.1"];
305                                 if (ext != null) {
306                                         string text = ext.NetscapeCertType (false);
307                                         return text.IndexOf ("SSL Server Authentication", StringComparison.Ordinal) != -1;
308                                 }
309                                 return true;
310                         } catch (Exception e) {
311                                 Console.Error.WriteLine ("ERROR processing certificate: {0}", e);
312                                 Console.Error.WriteLine ("Please, report this problem to the Mono team");
313                                 return false;
314                         }
315                 }
316
317                 // RFC2818 - HTTP Over TLS, Section 3.1
318                 // http://www.ietf.org/rfc/rfc2818.txt
319                 //
320                 // 1.   if present MUST use subjectAltName dNSName as identity
321                 // 1.1.         if multiples entries a match of any one is acceptable
322                 // 1.2.         wildcard * is acceptable
323                 // 2.   URI may be an IP address -> subjectAltName.iPAddress
324                 // 2.1.         exact match is required
325                 // 3.   Use of the most specific Common Name (CN=) in the Subject
326                 // 3.1          Existing practice but DEPRECATED
327                 static bool CheckServerIdentity (X509Certificate2 cert, string targetHost)
328                 {
329                         try {
330                                 var mcert = new MSX.X509Certificate (cert.RawData);
331                                 MSX.X509Extension ext = mcert.Extensions ["2.5.29.17"];
332                                 // 1. subjectAltName
333                                 if (ext != null) {
334                                         SubjectAltNameExtension subjectAltName = new SubjectAltNameExtension (ext);
335                                         // 1.1 - multiple dNSName
336                                         foreach (string dns in subjectAltName.DNSNames) {
337                                                 // 1.2 TODO - wildcard support
338                                                 if (Match (targetHost, dns))
339                                                         return true;
340                                         }
341                                         // 2. ipAddress
342                                         foreach (string ip in subjectAltName.IPAddresses) {
343                                                 // 2.1. Exact match required
344                                                 if (ip == targetHost)
345                                                         return true;
346                                         }
347                                 }
348                                 // 3. Common Name (CN=)
349                                 return CheckDomainName (mcert.SubjectName, targetHost);
350                         } catch (Exception e) {
351                                 Console.Error.WriteLine ("ERROR processing certificate: {0}", e);
352                                 Console.Error.WriteLine ("Please, report this problem to the Mono team");
353                                 return false;
354                         }
355                 }
356
357                 static bool CheckDomainName (string subjectName, string targetHost)
358                 {
359                         string  domainName = String.Empty;
360                         Regex search = new Regex (@"CN\s*=\s*([^,]*)");
361                         MatchCollection elements = search.Matches (subjectName);
362                         if (elements.Count == 1) {
363                                 if (elements [0].Success)
364                                         domainName = elements [0].Groups [1].Value.ToString ();
365                         }
366
367                         return Match (targetHost, domainName);
368                 }
369
370                 // ensure the pattern is valid wrt to RFC2595 and RFC2818
371                 // http://www.ietf.org/rfc/rfc2595.txt
372                 // http://www.ietf.org/rfc/rfc2818.txt
373                 static bool Match (string hostname, string pattern)
374                 {
375                         // check if this is a pattern
376                         int index = pattern.IndexOf ('*');
377                         if (index == -1) {
378                                 // not a pattern, do a direct case-insensitive comparison
379                                 return (String.Compare (hostname, pattern, true, CultureInfo.InvariantCulture) == 0);
380                         }
381
382                         // check pattern validity
383                         // A "*" wildcard character MAY be used as the left-most name component in the certificate.
384
385                         // unless this is the last char (valid)
386                         if (index != pattern.Length - 1) {
387                                 // then the next char must be a dot .'.
388                                 if (pattern [index + 1] != '.')
389                                         return false;
390                         }
391
392                         // only one (A) wildcard is supported
393                         int i2 = pattern.IndexOf ('*', index + 1);
394                         if (i2 != -1)
395                                 return false;
396
397                         // match the end of the pattern
398                         string end = pattern.Substring (index + 1);
399                         int length = hostname.Length - end.Length;
400                         // no point to check a pattern that is longer than the hostname
401                         if (length <= 0)
402                                 return false;
403
404                         if (String.Compare (hostname, length, end, 0, end.Length, true, CultureInfo.InvariantCulture) != 0)
405                                 return false;
406
407                         // special case, we start with the wildcard
408                         if (index == 0) {
409                                 // ensure we hostname non-matched part (start) doesn't contain a dot
410                                 int i3 = hostname.IndexOf ('.');
411                                 return ((i3 == -1) || (i3 >= (hostname.Length - end.Length)));
412                         }
413
414                         // match the start of the pattern
415                         string start = pattern.Substring (0, index);
416                         return (String.Compare (hostname, 0, start, 0, start.Length, true, CultureInfo.InvariantCulture) == 0);
417                 }
418 #endif
419         }
420 }
421 #endif
422