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