1 //------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation.  All rights reserved.
3 //------------------------------------------------------------
4 
5 namespace System.IdentityModel.Claims
6 {
7     using System.Collections.Generic;
8     using System.Diagnostics;
9     using System.IdentityModel.Policy;
10     using System.Net.Mail;
11     using System.Security.Claims;
12     using System.Security.Cryptography;
13     using System.Security.Cryptography.X509Certificates;
14     using System.Security.Principal;
15     using Globalization;
16 
17     public class X509CertificateClaimSet : ClaimSet, IIdentityInfo, IDisposable
18     {
19         X509Certificate2 certificate;
20         DateTime expirationTime = SecurityUtils.MinUtcDateTime;
21         ClaimSet issuer;
22         X509Identity identity;
23         X509ChainElementCollection elements;
24         IList<Claim> claims;
25         int index;
26         bool disposed = false;
27 
X509CertificateClaimSet(X509Certificate2 certificate)28         public X509CertificateClaimSet(X509Certificate2 certificate)
29             : this(certificate, true)
30         {
31         }
32 
X509CertificateClaimSet(X509Certificate2 certificate, bool clone)33         internal X509CertificateClaimSet(X509Certificate2 certificate, bool clone)
34         {
35             if (certificate == null)
36                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("certificate");
37             this.certificate = clone ? new X509Certificate2(certificate) : certificate;
38         }
39 
X509CertificateClaimSet(X509CertificateClaimSet from)40         X509CertificateClaimSet(X509CertificateClaimSet from)
41             : this(from.X509Certificate, true)
42         {
43         }
44 
X509CertificateClaimSet(X509ChainElementCollection elements, int index)45         X509CertificateClaimSet(X509ChainElementCollection elements, int index)
46         {
47             this.elements = elements;
48             this.index = index;
49             this.certificate = elements[index].Certificate;
50         }
51 
52         public override Claim this[int index]
53         {
54             get
55             {
56                 ThrowIfDisposed();
57                 EnsureClaims();
58                 return this.claims[index];
59             }
60         }
61 
62         public override int Count
63         {
64             get
65             {
66                 ThrowIfDisposed();
67                 EnsureClaims();
68                 return this.claims.Count;
69             }
70         }
71 
72         IIdentity IIdentityInfo.Identity
73         {
74             get
75             {
76                 ThrowIfDisposed();
77                 if (this.identity == null)
78                     this.identity = new X509Identity(this.certificate, false, false);
79                 return this.identity;
80             }
81         }
82 
83         public DateTime ExpirationTime
84         {
85             get
86             {
87                 ThrowIfDisposed();
88                 if (this.expirationTime == SecurityUtils.MinUtcDateTime)
89                     this.expirationTime = this.certificate.NotAfter.ToUniversalTime();
90                 return this.expirationTime;
91             }
92         }
93 
94         public override ClaimSet Issuer
95         {
96             get
97             {
98                 ThrowIfDisposed();
99                 if (this.issuer == null)
100                 {
101                     if (this.elements == null)
102                     {
103                         X509Chain chain = new X509Chain();
104                         chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
105                         chain.Build(certificate);
106                         this.index = 0;
107                         this.elements = chain.ChainElements;
108                     }
109 
110                     if (this.index + 1 < this.elements.Count)
111                     {
112                         this.issuer = new X509CertificateClaimSet(this.elements, this.index + 1);
113                         this.elements = null;
114                     }
115                     // SelfSigned?
116                     else if (StringComparer.OrdinalIgnoreCase.Equals(this.certificate.SubjectName.Name, this.certificate.IssuerName.Name))
117                         this.issuer = this;
118                     else
119                         this.issuer = new X500DistinguishedNameClaimSet(this.certificate.IssuerName);
120 
121                 }
122                 return this.issuer;
123             }
124         }
125 
126         public X509Certificate2 X509Certificate
127         {
128             get
129             {
130                 ThrowIfDisposed();
131                 return this.certificate;
132             }
133         }
134 
Clone()135         internal X509CertificateClaimSet Clone()
136         {
137             ThrowIfDisposed();
138             return new X509CertificateClaimSet(this);
139         }
140 
Dispose()141         public void Dispose()
142         {
143             if (!this.disposed)
144             {
145                 this.disposed = true;
146                 SecurityUtils.DisposeIfNecessary(this.identity);
147                 if (this.issuer != null)
148                 {
149                     if (this.issuer != this)
150                     {
151                         SecurityUtils.DisposeIfNecessary(this.issuer as IDisposable);
152                     }
153                 }
154                 if (this.elements != null)
155                 {
156                     for (int i = this.index + 1; i < this.elements.Count; ++i)
157                     {
158                         SecurityUtils.ResetCertificate(this.elements[i].Certificate);
159                     }
160                 }
161                 SecurityUtils.ResetCertificate(this.certificate);
162             }
163         }
164 
InitializeClaimsCore()165         IList<Claim> InitializeClaimsCore()
166         {
167             List<Claim> claims = new List<Claim>();
168             byte[] thumbprint = this.certificate.GetCertHash();
169             claims.Add(new Claim(ClaimTypes.Thumbprint, thumbprint, Rights.Identity));
170             claims.Add(new Claim(ClaimTypes.Thumbprint, thumbprint, Rights.PossessProperty));
171 
172             // Ordering SubjectName, Dns, SimpleName, Email, Upn
173             string value = this.certificate.SubjectName.Name;
174             if (!string.IsNullOrEmpty(value))
175                 claims.Add(Claim.CreateX500DistinguishedNameClaim(this.certificate.SubjectName));
176 
177             claims.AddRange(GetDnsClaims(this.certificate));
178 
179             value = this.certificate.GetNameInfo(X509NameType.SimpleName, false);
180             if (!string.IsNullOrEmpty(value))
181                 claims.Add(Claim.CreateNameClaim(value));
182 
183             value = this.certificate.GetNameInfo(X509NameType.EmailName, false);
184             if (!string.IsNullOrEmpty(value))
185                 claims.Add(Claim.CreateMailAddressClaim(new MailAddress(value)));
186 
187             value = this.certificate.GetNameInfo(X509NameType.UpnName, false);
188             if (!string.IsNullOrEmpty(value))
189                 claims.Add(Claim.CreateUpnClaim(value));
190 
191             value = this.certificate.GetNameInfo(X509NameType.UrlName, false);
192             if (!string.IsNullOrEmpty(value))
193                 claims.Add(Claim.CreateUriClaim(new Uri(value)));
194 
195             RSA rsa;
196             if (LocalAppContextSwitches.DisableCngCertificates)
197             {
198                 rsa = this.certificate.PublicKey.Key as RSA;
199             }
200             else
201             {
202                 rsa = CngLightup.GetRSAPublicKey(this.certificate);
203             }
204             if (rsa != null)
205                 claims.Add(Claim.CreateRsaClaim(rsa));
206 
207             return claims;
208         }
209 
EnsureClaims()210         void EnsureClaims()
211         {
212             if (this.claims != null)
213                 return;
214 
215             this.claims = InitializeClaimsCore();
216         }
217 
SupportedClaimType(string claimType)218         static bool SupportedClaimType(string claimType)
219         {
220             return claimType == null ||
221                 ClaimTypes.Thumbprint.Equals(claimType) ||
222                 ClaimTypes.X500DistinguishedName.Equals(claimType) ||
223                 ClaimTypes.Dns.Equals(claimType) ||
224                 ClaimTypes.Name.Equals(claimType) ||
225                 ClaimTypes.Email.Equals(claimType) ||
226                 ClaimTypes.Upn.Equals(claimType) ||
227                 ClaimTypes.Uri.Equals(claimType) ||
228                 ClaimTypes.Rsa.Equals(claimType);
229         }
230 
231         // Note: null string represents any.
FindClaims(string claimType, string right)232         public override IEnumerable<Claim> FindClaims(string claimType, string right)
233         {
234             ThrowIfDisposed();
235             if (!SupportedClaimType(claimType) || !ClaimSet.SupportedRight(right))
236             {
237                 yield break;
238             }
239             else if (this.claims == null && ClaimTypes.Thumbprint.Equals(claimType))
240             {
241                 if (right == null || Rights.Identity.Equals(right))
242                 {
243                     yield return new Claim(ClaimTypes.Thumbprint, this.certificate.GetCertHash(), Rights.Identity);
244                 }
245                 if (right == null || Rights.PossessProperty.Equals(right))
246                 {
247                     yield return new Claim(ClaimTypes.Thumbprint, this.certificate.GetCertHash(), Rights.PossessProperty);
248                 }
249             }
250             else if (this.claims == null && ClaimTypes.Dns.Equals(claimType))
251             {
252                 if (right == null || Rights.PossessProperty.Equals(right))
253                 {
254                     foreach (var claim in GetDnsClaims(certificate))
255                         yield return claim;
256                 }
257             }
258             else
259             {
260                 EnsureClaims();
261 
262                 bool anyClaimType = (claimType == null);
263                 bool anyRight = (right == null);
264 
265                 for (int i = 0; i < this.claims.Count; ++i)
266                 {
267                     Claim claim = this.claims[i];
268                     if ((claim != null) &&
269                         (anyClaimType || claimType.Equals(claim.ClaimType)) &&
270                         (anyRight || right.Equals(claim.Right)))
271                     {
272                         yield return claim;
273                     }
274                 }
275             }
276         }
277 
GetDnsClaims(X509Certificate2 cert)278         private static List<Claim> GetDnsClaims(X509Certificate2 cert)
279         {
280             List<Claim> dnsClaimEntries = new List<Claim>();
281 
282             // old behavior, default for <= 4.6
283             string value = cert.GetNameInfo(X509NameType.DnsName, false);
284             if (!string.IsNullOrEmpty(value))
285                 dnsClaimEntries.Add(Claim.CreateDnsClaim(value));
286 
287             // App context switch for disabling support for multiple dns entries in a SAN certificate
288             // If we can't dynamically parse the alt subject names, we will not add any dns claims ONLY for the alt subject names.
289             // In this way, if the X509NameType.DnsName was enough to succeed for the out-bound-message. We would have a success.
290             if (!LocalAppContextSwitches.DisableMultipleDNSEntriesInSANCertificate && X509SubjectAlternativeNameConstants.SuccessfullyInitialized)
291             {
292                 foreach (X509Extension ext in cert.Extensions)
293                 {
294                     // Extension is SAN or SAN2
295                     if (ext.Oid.Value == X509SubjectAlternativeNameConstants.SanOid || ext.Oid.Value == X509SubjectAlternativeNameConstants.San2Oid)
296                     {
297                         string asnString = ext.Format(false);
298                         if (string.IsNullOrWhiteSpace(asnString))
299                             break;
300 
301                         // SubjectAlternativeNames might contain something other than a dNSName,
302                         // so we have to parse through and only use the dNSNames
303                         // <identifier><delimiter><value><separator(s)>
304                         string[] rawDnsEntries = asnString.Split(X509SubjectAlternativeNameConstants.SeparatorArray, StringSplitOptions.RemoveEmptyEntries);
305                         for (int i = 0; i < rawDnsEntries.Length; i++)
306                         {
307                             string[] keyval = rawDnsEntries[i].Split(X509SubjectAlternativeNameConstants.Delimiter);
308                             if (string.Equals(keyval[0], X509SubjectAlternativeNameConstants.Identifier))
309                                 dnsClaimEntries.Add(Claim.CreateDnsClaim(keyval[1]));
310                         }
311                     }
312                 }
313             }
314 
315             return dnsClaimEntries;
316         }
317 
GetEnumerator()318         public override IEnumerator<Claim> GetEnumerator()
319         {
320             ThrowIfDisposed();
321             EnsureClaims();
322             return this.claims.GetEnumerator();
323         }
324 
ToString()325         public override string ToString()
326         {
327             return this.disposed ? base.ToString() : SecurityUtils.ClaimSetToString(this);
328         }
329 
ThrowIfDisposed()330         void ThrowIfDisposed()
331         {
332             if (this.disposed)
333             {
334                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ObjectDisposedException(this.GetType().FullName));
335             }
336         }
337 
338         class X500DistinguishedNameClaimSet : DefaultClaimSet, IIdentityInfo
339         {
340             IIdentity identity;
341 
X500DistinguishedNameClaimSet(X500DistinguishedName x500DistinguishedName)342             public X500DistinguishedNameClaimSet(X500DistinguishedName x500DistinguishedName)
343             {
344                 if (x500DistinguishedName == null)
345                     throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("x500DistinguishedName");
346 
347                 this.identity = new X509Identity(x500DistinguishedName);
348                 List<Claim> claims = new List<Claim>(2);
349                 claims.Add(new Claim(ClaimTypes.X500DistinguishedName, x500DistinguishedName, Rights.Identity));
350                 claims.Add(Claim.CreateX500DistinguishedNameClaim(x500DistinguishedName));
351                 Initialize(ClaimSet.Anonymous, claims);
352             }
353 
354             public IIdentity Identity
355             {
356                 get { return this.identity; }
357             }
358         }
359 
360         // We don't have a strongly typed extension to parse Subject Alt Names, so we have to do a workaround
361         // to figure out what the identifier, delimiter, and separator is by using a well-known extension
362         private static class X509SubjectAlternativeNameConstants
363         {
364             public const string SanOid = "2.5.29.7";
365             public const string San2Oid = "2.5.29.17";
366 
367             public static string Identifier
368             {
369                 get;
370                 private set;
371             }
372 
373             public static char Delimiter
374             {
375                 get;
376                 private set;
377             }
378 
379             public static string Separator
380             {
381                 get;
382                 private set;
383             }
384 
385             public static string[] SeparatorArray
386             {
387                 get;
388                 private set;
389             }
390 
391             public static bool SuccessfullyInitialized
392             {
393                 get;
394                 private set;
395             }
396 
397             // static initializer will run before properties are accessed
X509SubjectAlternativeNameConstants()398             static X509SubjectAlternativeNameConstants()
399             {
400                 // Extracted a well-known X509Extension
401                 byte[] x509ExtensionBytes = new byte[] {
402                     48, 36, 130, 21, 110, 111, 116, 45, 114, 101, 97, 108, 45, 115, 117, 98, 106, 101, 99,
403                     116, 45, 110, 97, 109, 101, 130, 11, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109
404                 };
405                 const string subjectName = "not-real-subject-name";
406                 string x509ExtensionFormattedString = string.Empty;
407                 try
408                 {
409                     X509Extension x509Extension = new X509Extension(SanOid, x509ExtensionBytes, true);
410                     x509ExtensionFormattedString = x509Extension.Format(false);
411 
412                     // Each OS has a different dNSName identifier and delimiter
413                     // On Windows, dNSName == "DNS Name" (localizable), on Linux, dNSName == "DNS"
414                     // e.g.,
415                     // Windows: x509ExtensionFormattedString is: "DNS Name=not-real-subject-name, DNS Name=example.com"
416                     // Linux:   x509ExtensionFormattedString is: "DNS:not-real-subject-name, DNS:example.com"
417                     // Parse: <identifier><delimiter><value><separator(s)>
418 
419                     int delimiterIndex = x509ExtensionFormattedString.IndexOf(subjectName) - 1;
420                     Delimiter = x509ExtensionFormattedString[delimiterIndex];
421 
422                     // Make an assumption that all characters from the the start of string to the delimiter
423                     // are part of the identifier
424                     Identifier = x509ExtensionFormattedString.Substring(0, delimiterIndex);
425 
426                     int separatorFirstChar = delimiterIndex + subjectName.Length + 1;
427                     int separatorLength = 1;
428                     for (int i = separatorFirstChar + 1; i < x509ExtensionFormattedString.Length; i++)
429                     {
430                         // We advance until the first character of the identifier to determine what the
431                         // separator is. This assumes that the identifier assumption above is correct
432                         if (x509ExtensionFormattedString[i] == Identifier[0])
433                         {
434                             break;
435                         }
436 
437                         separatorLength++;
438                     }
439 
440                     Separator = x509ExtensionFormattedString.Substring(separatorFirstChar, separatorLength);
441                     SeparatorArray = new string[1] { Separator };
442                     SuccessfullyInitialized = true;
443                 }
444                 catch (Exception ex)
445                 {
446                     SuccessfullyInitialized = false;
447                     DiagnosticUtility.TraceHandledException(
448                         new FormatException(string.Format(CultureInfo.InvariantCulture,
449                         "There was an error parsing the SubjectAlternativeNames: '{0}'. See inner exception for more details.{1}Detected values were: Identifier: '{2}'; Delimiter:'{3}'; Separator:'{4}'",
450                         x509ExtensionFormattedString,
451                         Environment.NewLine,
452                         Identifier,
453                         Delimiter,
454                         Separator),
455                         ex),
456                         TraceEventType.Warning);
457                 }
458             }
459         }
460     }
461 
462     class X509Identity : GenericIdentity, IDisposable
463     {
464         const string X509 = "X509";
465         const string Thumbprint = "; ";
466         X500DistinguishedName x500DistinguishedName;
467         X509Certificate2 certificate;
468         string name;
469         bool disposed = false;
470         bool disposable = true;
471 
X509Identity(X509Certificate2 certificate)472         public X509Identity(X509Certificate2 certificate)
473             : this(certificate, true, true)
474         {
475         }
476 
X509Identity(X500DistinguishedName x500DistinguishedName)477         public X509Identity(X500DistinguishedName x500DistinguishedName)
478             : base(X509, X509)
479         {
480             this.x500DistinguishedName = x500DistinguishedName;
481         }
482 
X509Identity(X509Certificate2 certificate, bool clone, bool disposable)483         internal X509Identity(X509Certificate2 certificate, bool clone, bool disposable)
484             : base(X509, X509)
485         {
486             this.certificate = clone ? new X509Certificate2(certificate) : certificate;
487             this.disposable = clone || disposable;
488         }
489 
490         public override string Name
491         {
492             get
493             {
494                 ThrowIfDisposed();
495                 if (this.name == null)
496                 {
497                     //
498                     // DCR 48092: PrincipalPermission authorization using certificates could cause Elevation of Privilege.
499                     // because there could be duplicate subject name.  In order to be more unique, we use SubjectName + Thumbprint
500                     // instead
501                     //
502                     this.name = GetName() + Thumbprint + this.certificate.Thumbprint;
503                 }
504                 return this.name;
505             }
506         }
507 
GetName()508         string GetName()
509         {
510             if (this.x500DistinguishedName != null)
511                 return this.x500DistinguishedName.Name;
512 
513             string value = this.certificate.SubjectName.Name;
514             if (!string.IsNullOrEmpty(value))
515                 return value;
516 
517             value = this.certificate.GetNameInfo(X509NameType.DnsName, false);
518             if (!string.IsNullOrEmpty(value))
519                 return value;
520 
521             value = this.certificate.GetNameInfo(X509NameType.SimpleName, false);
522             if (!string.IsNullOrEmpty(value))
523                 return value;
524 
525             value = this.certificate.GetNameInfo(X509NameType.EmailName, false);
526             if (!string.IsNullOrEmpty(value))
527                 return value;
528 
529             value = this.certificate.GetNameInfo(X509NameType.UpnName, false);
530             if (!string.IsNullOrEmpty(value))
531                 return value;
532 
533             return String.Empty;
534         }
535 
Clone()536         public override ClaimsIdentity Clone()
537         {
538             return this.certificate != null ? new X509Identity(this.certificate) : new X509Identity(this.x500DistinguishedName);
539         }
540 
Dispose()541         public void Dispose()
542         {
543             if (this.disposable && !this.disposed)
544             {
545                 this.disposed = true;
546                 if (this.certificate != null)
547                 {
548                     this.certificate.Reset();
549                 }
550             }
551         }
552 
ThrowIfDisposed()553         void ThrowIfDisposed()
554         {
555             if (this.disposed)
556             {
557                 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ObjectDisposedException(this.GetType().FullName));
558             }
559         }
560     }
561 }
562