1 //----------------------------------------------------------------------------- 2 // Copyright (c) Microsoft Corporation. All rights reserved. 3 //----------------------------------------------------------------------------- 4 5 namespace System.ServiceModel.Security 6 { 7 using System.Runtime.InteropServices; 8 using System.ServiceModel.Channels; 9 using System.ServiceModel; 10 using System.ServiceModel.Diagnostics; 11 using System.Diagnostics; 12 using System.Collections.Generic; 13 using System.Text; 14 using System.Threading; 15 using System.Globalization; 16 using System.ComponentModel; 17 using System.Security.Principal; 18 using System.IdentityModel.Tokens; 19 using System.Net; 20 using System.IdentityModel; 21 using System.IdentityModel.Selectors; 22 using System.Security.Authentication.ExtendedProtection; 23 using IMD = System.IdentityModel.Diagnostics; 24 25 using DiagnosticUtility = System.ServiceModel.DiagnosticUtility; 26 using SR = System.ServiceModel.SR; 27 28 internal sealed class WindowsSspiNegotiation : ISspiNegotiation 29 { 30 const int DefaultMaxPromptAttempts = 1; 31 SspiContextFlags contextFlags; 32 SafeFreeCredentials credentialsHandle; 33 bool disposed = false; 34 bool doMutualAuth; 35 TokenImpersonationLevel impersonationLevel; 36 bool isCompleted; 37 bool isServer; 38 LifeSpan lifespan; 39 string protocolName; 40 SafeDeleteContext securityContext; 41 string servicePrincipalName; 42 SecSizes sizes; 43 Object syncObject = new Object(); 44 int tokenSize; 45 bool interactiveNegoLogonEnabled = true; 46 string clientPackageName; 47 bool saveClientCredentialsOnSspiUi = true; 48 bool allowNtlm; 49 int MaxPromptAttempts = 0; 50 51 /// <summary> 52 /// Client side ctor 53 /// </summary> WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled)54 internal WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled) 55 : this(false, package, credentialsHandle, impersonationLevel, servicePrincipalName, doMutualAuth, interactiveLogonEnabled, ntlmEnabled) 56 { } 57 58 /// <summary> 59 /// Server side ctor 60 /// </summary> WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, string defaultServiceBinding)61 internal WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, string defaultServiceBinding) 62 : this(true, package, credentialsHandle, TokenImpersonationLevel.Delegation, defaultServiceBinding, false, false, true) 63 { } 64 WindowsSspiNegotiation(bool isServer, string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled)65 WindowsSspiNegotiation(bool isServer, string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled) 66 { 67 this.tokenSize = SspiWrapper.GetVerifyPackageInfo(package).MaxToken; 68 this.isServer = isServer; 69 this.servicePrincipalName = servicePrincipalName; 70 this.securityContext = null; 71 if (isServer) 72 { 73 this.impersonationLevel = TokenImpersonationLevel.Delegation; 74 this.doMutualAuth = false; 75 } 76 else 77 { 78 this.impersonationLevel = impersonationLevel; 79 this.doMutualAuth = doMutualAuth; 80 this.interactiveNegoLogonEnabled = interactiveLogonEnabled; 81 this.clientPackageName = package; 82 this.allowNtlm = ntlmEnabled; 83 } 84 this.credentialsHandle = credentialsHandle; 85 } 86 87 public DateTime ExpirationTimeUtc 88 { 89 get 90 { 91 ThrowIfDisposed(); 92 if (this.LifeSpan == null) 93 { 94 return SecurityUtils.MaxUtcDateTime; 95 } 96 else 97 { 98 return this.LifeSpan.ExpiryTimeUtc; 99 } 100 } 101 } 102 103 public bool IsCompleted 104 { 105 get 106 { 107 ThrowIfDisposed(); 108 return this.isCompleted; 109 } 110 } 111 112 public bool IsDelegationFlag 113 { 114 get 115 { 116 ThrowIfDisposed(); 117 return (this.contextFlags & SspiContextFlags.Delegate) != 0; 118 } 119 } 120 121 public bool IsIdentifyFlag 122 { 123 get 124 { 125 ThrowIfDisposed(); 126 return (this.contextFlags & (this.isServer ? SspiContextFlags.AcceptIdentify : SspiContextFlags.InitIdentify)) != 0; 127 } 128 } 129 130 public bool IsMutualAuthFlag 131 { 132 get 133 { 134 ThrowIfDisposed(); 135 return (this.contextFlags & SspiContextFlags.MutualAuth) != 0; 136 } 137 } 138 139 public bool IsValidContext 140 { 141 get 142 { 143 return (this.securityContext != null && this.securityContext.IsInvalid == false); 144 } 145 } 146 147 public string KeyEncryptionAlgorithm 148 { 149 get 150 { 151 return SecurityAlgorithms.WindowsSspiKeyWrap; 152 } 153 } 154 155 public LifeSpan LifeSpan 156 { 157 get 158 { 159 ThrowIfDisposed(); 160 if (this.lifespan == null) 161 { 162 LifeSpan tmpLifeSpan = (LifeSpan)SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.Lifespan); 163 164 if (IsCompleted) 165 { 166 // cache it only when it's completed 167 this.lifespan = tmpLifeSpan; 168 } 169 170 return tmpLifeSpan; 171 } 172 173 return this.lifespan; 174 } 175 } 176 177 public string ProtocolName 178 { 179 get 180 { 181 ThrowIfDisposed(); 182 if (this.protocolName == null) 183 { 184 NegotiationInfoClass negotiationInfo = SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.NegotiationInfo) as NegotiationInfoClass; 185 186 if (IsCompleted) 187 { 188 // cache it only when it's completed 189 this.protocolName = negotiationInfo.AuthenticationPackage; 190 } 191 192 return negotiationInfo.AuthenticationPackage; 193 } 194 195 return this.protocolName; 196 } 197 } 198 199 public string ServicePrincipalName 200 { 201 get 202 { 203 ThrowIfDisposed(); 204 return this.servicePrincipalName; 205 } 206 } 207 208 SecSizes SecuritySizes 209 { 210 get 211 { 212 ThrowIfDisposed(); 213 if (this.sizes == null) 214 { 215 SecSizes tmpSizes = (SecSizes)SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.Sizes); 216 217 if (IsCompleted) 218 { 219 // cache it only when it's completed 220 this.sizes = tmpSizes; 221 } 222 223 return tmpSizes; 224 } 225 226 return this.sizes; 227 } 228 } 229 GetRemoteIdentityName()230 public string GetRemoteIdentityName() 231 { 232 if (!this.isServer) 233 { 234 return this.servicePrincipalName; 235 } 236 237 if (IsValidContext) 238 { 239 using (SafeCloseHandle contextToken = GetContextToken()) 240 { 241 using (WindowsIdentity windowsIdentity = new WindowsIdentity(contextToken.DangerousGetHandle(), this.ProtocolName)) 242 { 243 return windowsIdentity.Name; 244 } 245 } 246 } 247 return String.Empty; 248 } 249 Decrypt(byte[] encryptedContent)250 public byte[] Decrypt(byte[] encryptedContent) 251 { 252 if (encryptedContent == null) 253 throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("encryptedContent"); 254 ThrowIfDisposed(); 255 256 SecurityBuffer[] securityBuffer = new SecurityBuffer[2]; 257 securityBuffer[0] = new SecurityBuffer(encryptedContent, 0, encryptedContent.Length, BufferType.Stream); 258 securityBuffer[1] = new SecurityBuffer(0, BufferType.Data); 259 int errorCode = SspiWrapper.DecryptMessage(this.securityContext, securityBuffer, 0, true); 260 if (errorCode != 0) 261 { 262 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(errorCode)); 263 } 264 265 for (int i = 0; i < securityBuffer.Length; ++i) 266 { 267 if (securityBuffer[i].type == BufferType.Data) 268 { 269 return securityBuffer[i].token; 270 } 271 } 272 OnBadData(); 273 return null; 274 } 275 Dispose()276 public void Dispose() 277 { 278 Dispose(true); 279 GC.SuppressFinalize(this); 280 } 281 Encrypt(byte[] input)282 public byte[] Encrypt(byte[] input) 283 { 284 if (input == null) 285 throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("input"); 286 ThrowIfDisposed(); 287 SecurityBuffer[] securityBuffer = new SecurityBuffer[3]; 288 289 byte[] tokenBuffer = DiagnosticUtility.Utility.AllocateByteArray(SecuritySizes.SecurityTrailer); 290 securityBuffer[0] = new SecurityBuffer(tokenBuffer, 0, tokenBuffer.Length, BufferType.Token); 291 byte[] dataBuffer = DiagnosticUtility.Utility.AllocateByteArray(input.Length); 292 Buffer.BlockCopy(input, 0, dataBuffer, 0, input.Length); 293 securityBuffer[1] = new SecurityBuffer(dataBuffer, 0, dataBuffer.Length, BufferType.Data); 294 byte[] paddingBuffer = DiagnosticUtility.Utility.AllocateByteArray(SecuritySizes.BlockSize); 295 securityBuffer[2] = new SecurityBuffer(paddingBuffer, 0, paddingBuffer.Length, BufferType.Padding); 296 297 int errorCode = SspiWrapper.EncryptMessage(this.securityContext, securityBuffer, 0); 298 if (errorCode != 0) 299 { 300 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(errorCode)); 301 } 302 303 int tokenLen = 0; 304 int paddingLen = 0; 305 for (int i = 0; i < securityBuffer.Length; ++i) 306 { 307 if (securityBuffer[i].type == BufferType.Token) 308 tokenLen = securityBuffer[i].size; 309 else if (securityBuffer[i].type == BufferType.Padding) 310 paddingLen = securityBuffer[i].size; 311 } 312 byte[] encryptedData = DiagnosticUtility.Utility.AllocateByteArray(checked(tokenLen + dataBuffer.Length + paddingLen)); 313 314 Buffer.BlockCopy(tokenBuffer, 0, encryptedData, 0, tokenLen); 315 Buffer.BlockCopy(dataBuffer, 0, encryptedData, tokenLen, dataBuffer.Length); 316 Buffer.BlockCopy(paddingBuffer, 0, encryptedData, tokenLen + dataBuffer.Length, paddingLen); 317 318 return encryptedData; 319 } 320 GetOutgoingBlob(byte[] incomingBlob, ChannelBinding channelbinding, ExtendedProtectionPolicy protectionPolicy)321 public byte[] GetOutgoingBlob(byte[] incomingBlob, ChannelBinding channelbinding, ExtendedProtectionPolicy protectionPolicy) 322 { 323 ThrowIfDisposed(); 324 int statusCode = 0; 325 326 // use the confidentiality option to ensure we can encrypt messages 327 SspiContextFlags requestedFlags = SspiContextFlags.Confidentiality 328 | SspiContextFlags.ReplayDetect 329 | SspiContextFlags.SequenceDetect; 330 331 if (this.doMutualAuth) 332 { 333 requestedFlags |= SspiContextFlags.MutualAuth; 334 } 335 336 if (this.impersonationLevel == TokenImpersonationLevel.Delegation) 337 { 338 requestedFlags |= SspiContextFlags.Delegate; 339 } 340 else if (this.isServer == false && this.impersonationLevel == TokenImpersonationLevel.Identification) 341 { 342 requestedFlags |= SspiContextFlags.InitIdentify; 343 } 344 else if (this.isServer == false && this.impersonationLevel == TokenImpersonationLevel.Anonymous) 345 { 346 requestedFlags |= SspiContextFlags.InitAnonymous; 347 } 348 349 ExtendedProtectionPolicyHelper policyHelper = new ExtendedProtectionPolicyHelper(channelbinding, protectionPolicy); 350 351 if (isServer) 352 { 353 if (policyHelper.PolicyEnforcement == PolicyEnforcement.Always && policyHelper.ChannelBinding == null && policyHelper.ProtectionScenario != ProtectionScenario.TrustedProxy) 354 { 355 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.SecurityChannelBindingMissing))); 356 } 357 358 if (policyHelper.PolicyEnforcement == PolicyEnforcement.WhenSupported) 359 { 360 requestedFlags |= SspiContextFlags.ChannelBindingAllowMissingBindings; 361 } 362 363 if (policyHelper.ProtectionScenario == ProtectionScenario.TrustedProxy) 364 { 365 requestedFlags |= SspiContextFlags.ChannelBindingProxyBindings; 366 } 367 } 368 369 List<SecurityBuffer> list = new List<SecurityBuffer>(2); 370 371 if (incomingBlob != null) 372 { 373 list.Add(new SecurityBuffer(incomingBlob, BufferType.Token)); 374 } 375 376 // when deciding if the channel binding should be added to the security buffer 377 // it is necessary to differentiate between client and server. 378 // Server rules were added to policyHelper as they are shared with Kerb and I want them consistent 379 // Client adds if not null. 380 if (this.isServer) 381 { 382 if (policyHelper.ShouldAddChannelBindingToASC()) 383 { 384 list.Add(new SecurityBuffer(policyHelper.ChannelBinding)); 385 } 386 } 387 else 388 { 389 if (policyHelper.ChannelBinding != null) 390 { 391 list.Add(new SecurityBuffer(policyHelper.ChannelBinding)); 392 } 393 } 394 395 SecurityBuffer[] inSecurityBuffer = null; 396 if (list.Count > 0) 397 { 398 inSecurityBuffer = list.ToArray(); 399 } 400 401 SecurityBuffer outSecurityBuffer = new SecurityBuffer(this.tokenSize, BufferType.Token); 402 403 if (!this.isServer) 404 { 405 //client session 406 statusCode = SspiWrapper.InitializeSecurityContext(this.credentialsHandle, 407 ref this.securityContext, 408 this.servicePrincipalName, 409 requestedFlags, 410 Endianness.Network, 411 inSecurityBuffer, 412 outSecurityBuffer, 413 ref this.contextFlags); 414 } 415 else 416 { 417 // server session 418 //This check is to save an unnecessary ASC call. 419 bool isServerSecurityContextNull = this.securityContext == null; 420 SspiContextFlags serverContextFlags = this.contextFlags; 421 422 statusCode = SspiWrapper.AcceptSecurityContext(this.credentialsHandle, 423 ref this.securityContext, 424 requestedFlags, 425 Endianness.Network, 426 inSecurityBuffer, 427 outSecurityBuffer, 428 ref this.contextFlags); 429 430 if (statusCode == (int)SecurityStatus.InvalidToken && !isServerSecurityContextNull) 431 { 432 // Call again into ASC after deleting the Securitycontext. If this securitycontext is not deleted 433 // then when the client sends NTLM blob the service will treat it as Nego2blob and will fail to authenticate the client. 434 this.contextFlags = serverContextFlags; 435 CloseContext(); 436 statusCode = SspiWrapper.AcceptSecurityContext(this.credentialsHandle, 437 ref this.securityContext, 438 requestedFlags, 439 Endianness.Network, 440 inSecurityBuffer, 441 outSecurityBuffer, 442 ref this.contextFlags); 443 } 444 } 445 446 if (DiagnosticUtility.ShouldTraceInformation) 447 { 448 IMD.SecurityTraceRecordHelper.TraceChannelBindingInformation(policyHelper, this.isServer, channelbinding); 449 } 450 451 if ((statusCode & unchecked((int)0x80000000)) != 0) 452 { 453 if (!this.isServer 454 && this.interactiveNegoLogonEnabled 455 && SecurityUtils.IsOSGreaterThanOrEqualToWin7() 456 && SspiWrapper.IsSspiPromptingNeeded((uint)statusCode) 457 && SspiWrapper.IsNegotiateExPackagePresent()) 458 { 459 // If we have prompted enough number of times (DefaultMaxPromptAttempts) with wrong credentials, then we do not prompt again and throw. 460 if (MaxPromptAttempts >= DefaultMaxPromptAttempts) 461 { 462 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.InvalidClientCredentials))); 463 } 464 465 IntPtr ppAuthIdentity = IntPtr.Zero; 466 uint errorCode = SspiWrapper.SspiPromptForCredential(this.servicePrincipalName, this.clientPackageName, out ppAuthIdentity, ref this.saveClientCredentialsOnSspiUi); 467 if (errorCode == (uint)CredentialStatus.Success) 468 { 469 IntPtr ppNewAuthIdentity = IntPtr.Zero; 470 471 if (!this.allowNtlm) 472 { 473 // When Ntlm is explicitly disabled we don't want the collected 474 //creds from the Kerb/NTLM tile to be used for NTLM auth. 475 476 uint status = UnsafeNativeMethods.SspiExcludePackage(ppAuthIdentity, "NTLM", out ppNewAuthIdentity); 477 } 478 else 479 { 480 ppNewAuthIdentity = ppAuthIdentity; 481 } 482 483 this.credentialsHandle = SspiWrapper.AcquireCredentialsHandle(this.clientPackageName, CredentialUse.Outbound, ref ppNewAuthIdentity); 484 485 if (IntPtr.Zero != ppNewAuthIdentity) 486 { 487 UnsafeNativeMethods.SspiFreeAuthIdentity(ppNewAuthIdentity); 488 } 489 490 CloseContext(); 491 492 MaxPromptAttempts++; 493 return this.GetOutgoingBlob(null, channelbinding, protectionPolicy); 494 } 495 else 496 { 497 // Call into SspiPromptForCredential had an error. Time to throw. 498 if (IntPtr.Zero != ppAuthIdentity) 499 { 500 UnsafeNativeMethods.SspiFreeAuthIdentity(ppAuthIdentity); 501 } 502 503 CloseContext(); 504 this.isCompleted = true; 505 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)errorCode, SR.GetString(SR.SspiErrorOrInvalidClientCredentials))); 506 } 507 } 508 509 CloseContext(); 510 this.isCompleted = true; 511 if (!this.isServer && (statusCode == (int)SecurityStatus.TargetUnknown 512 || statusCode == (int)SecurityStatus.WrongPrincipal)) 513 { 514 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.IncorrectSpnOrUpnSpecified, this.servicePrincipalName))); 515 } 516 else 517 { 518 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.InvalidSspiNegotiation))); 519 } 520 } 521 522 if (DiagnosticUtility.ShouldTraceInformation) 523 { 524 if (this.isServer) 525 { 526 SecurityTraceRecordHelper.TraceServiceOutgoingSpnego(this); 527 } 528 else 529 { 530 SecurityTraceRecordHelper.TraceClientOutgoingSpnego(this); 531 } 532 } 533 534 if (statusCode == (int)SecurityStatus.OK) 535 { 536 // we're done 537 this.isCompleted = true; 538 539 // These must all be true to check service binding 540 // 1. we are the service (listener) 541 // 2. caller is not anonymous 542 // 3. protocol is not Kerberos 543 // 4. policy is set to check service binding 544 // 545 if (isServer && ((this.contextFlags & SspiContextFlags.AcceptAnonymous) == 0) && (string.Compare(this.ProtocolName, NegotiationInfoClass.Kerberos, StringComparison.OrdinalIgnoreCase) != 0) && policyHelper.ShouldCheckServiceBinding) 546 { 547 // in the server case the servicePrincipalName is the defaultServiceBinding 548 549 if (DiagnosticUtility.ShouldTraceInformation) 550 { 551 string serviceBindingNameSentByClient; 552 SspiWrapper.QuerySpecifiedTarget(securityContext, out serviceBindingNameSentByClient); 553 IMD.SecurityTraceRecordHelper.TraceServiceNameBindingOnServer( serviceBindingNameSentByClient, this.servicePrincipalName, policyHelper.ServiceNameCollection); 554 } 555 556 policyHelper.CheckServiceBinding(this.securityContext, this.servicePrincipalName); 557 } 558 } 559 else 560 { 561 // we need to continue 562 } 563 564 return outSecurityBuffer.token; 565 } 566 ImpersonateContext()567 public void ImpersonateContext() 568 { 569 ThrowIfDisposed(); 570 if (!IsValidContext) 571 { 572 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)SecurityStatus.InvalidHandle)); 573 } 574 575 SspiWrapper.ImpersonateSecurityContext(this.securityContext); 576 } 577 CloseContext()578 internal void CloseContext() 579 { 580 ThrowIfDisposed(); 581 try 582 { 583 if (this.securityContext != null) 584 { 585 this.securityContext.Close(); 586 } 587 } 588 finally 589 { 590 this.securityContext = null; 591 } 592 } 593 Dispose(bool disposing)594 private void Dispose(bool disposing) 595 { 596 lock (this.syncObject) 597 { 598 if (this.disposed == false) 599 { 600 if (disposing) 601 { 602 this.CloseContext(); 603 } 604 605 // set to null any references that aren't finalizable 606 this.protocolName = null; 607 this.servicePrincipalName = null; 608 this.sizes = null; 609 this.disposed = true; 610 } 611 } 612 } 613 GetContextToken()614 internal SafeCloseHandle GetContextToken() 615 { 616 if (!IsValidContext) 617 { 618 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)SecurityStatus.InvalidHandle)); 619 } 620 621 SafeCloseHandle token; 622 SecurityStatus status = (SecurityStatus)SspiWrapper.QuerySecurityContextToken(this.securityContext, out token); 623 if (status != SecurityStatus.OK) 624 { 625 Utility.CloseInvalidOutSafeHandle(token); 626 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)status)); 627 } 628 return token; 629 } 630 OnBadData()631 void OnBadData() 632 { 633 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new MessageSecurityException(SR.GetString(SR.BadData))); 634 } 635 ThrowIfDisposed()636 void ThrowIfDisposed() 637 { 638 lock (this.syncObject) 639 { 640 if (this.disposed) 641 { 642 throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ObjectDisposedException(null)); 643 } 644 } 645 } 646 } 647 } 648