1 //------------------------------------------------------------------------------
2 // <copyright file="_DigestClient.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
8 using System.Net.Sockets;
9 using System.Collections;
10 using System.Collections.Generic;
12 using System.Security.Authentication.ExtendedProtection;
13 using System.Security.Cryptography;
14 using System.Security.Permissions;
15 using System.Globalization;
16 using System.Runtime.InteropServices;
17 using Microsoft.Win32;
19 using System.Security;
20 using System.Diagnostics;
22 internal class DigestClient : ISessionAuthenticationModule {
24 internal const string AuthType = "Digest";
25 internal static string Signature = AuthType.ToLower(CultureInfo.InvariantCulture);
26 internal static int SignatureSize = Signature.Length;
28 private static PrefixLookup challengeCache = new PrefixLookup();
29 private static readonly char[] singleSpaceArray = new char[]{' '};
31 // [....]: make sure WDigest fixes these bugs before we
32 // enable this code ("Windows OS" Product Studio database):
34 // 921024 1 Wdigest should support MD5, at least for explicit (non-default) credentials.
35 // 762116 2 WDigest should ignore directives that do not have a value
36 // 762115 3 WDigest should allow an application to retrieve the parsed domain directive
38 private static bool _WDigestAvailable;
40 static DigestClient() {
41 _WDigestAvailable = SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, NegotiationInfoClass.WDigest)!=null;
44 public Authorization Authenticate(string challenge, WebRequest webRequest, ICredentials credentials) {
45 GlobalLog.Print("DigestClient::Authenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()");
46 return DoAuthenticate(challenge, webRequest, credentials, false);
49 private Authorization DoAuthenticate(string challenge, WebRequest webRequest, ICredentials credentials, bool preAuthenticate) {
50 GlobalLog.Print("DigestClient::DoAuthenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " preAuthenticate:" + preAuthenticate.ToString());
52 GlobalLog.Assert(credentials != null, "DigestClient::DoAuthenticate()|credentials == null");
53 if (credentials==null) {
57 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
58 GlobalLog.Assert(httpWebRequest != null, "DigestClient::DoAuthenticate()|httpWebRequest == null");
59 GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "DigestClient::DoAuthenticate()|httpWebRequest.ChallengedUri == null");
61 // If it's default credentials, we support them on XP and up through WDigest.
64 NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, DigestClient.Signature);
65 GlobalLog.Print("DigestClient::DoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
67 if (NC is SystemNetworkCredential) {
68 if (WDigestAvailable) {
69 return XPDoAuthenticate(challenge, httpWebRequest, credentials, preAuthenticate);
76 HttpDigestChallenge digestChallenge;
77 if (!preAuthenticate) {
78 int index = AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
82 digestChallenge = HttpDigest.Interpret(challenge, index, httpWebRequest);
85 GlobalLog.Print("DigestClient::DoAuthenticate() looking up digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
86 digestChallenge = challengeCache.Lookup(httpWebRequest.ChallengedUri.AbsoluteUri) as HttpDigestChallenge;
88 if (digestChallenge==null) {
92 bool supported = CheckQOP(digestChallenge);
95 Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_qop_not_supported, digestChallenge.QualityOfProtection));
99 if (preAuthenticate) {
100 GlobalLog.Print("DigestClient::DoAuthenticate() retrieved digestChallenge#" + ValidationHelper.HashString(digestChallenge) + " digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
101 digestChallenge = digestChallenge.CopyAndIncrementNonce();
102 digestChallenge.SetFromRequest(httpWebRequest);
109 ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
110 if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
113 SpnToken spnToken = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest);
115 ChannelBinding binding = null;
116 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
118 binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
121 Authorization digestResponse = HttpDigest.Authenticate(digestChallenge, NC, spnToken.Spn, binding);
122 if (!preAuthenticate && webRequest.PreAuthenticate && digestResponse != null) {
123 // add this to the cache of challenges so we can preauthenticate
124 string[] prefixes = digestChallenge.Domain==null ?
125 new string[]{httpWebRequest.ChallengedUri.GetParts(UriComponents.SchemeAndServer, UriFormat.UriEscaped)}
126 : digestChallenge.Domain.Split(singleSpaceArray);
128 // If Domain property is not set we associate Digest only with the server Uri namespace (used to do with whole server)
129 digestResponse.ProtectionRealm = digestChallenge.Domain==null ? null: prefixes;
131 for (int i=0; i<prefixes.Length; i++) {
132 GlobalLog.Print("DigestClient::DoAuthenticate() adding digestChallenge#" + ValidationHelper.HashString(digestChallenge) + " for prefix:" + prefixes[i]);
133 challengeCache.Add(prefixes[i], digestChallenge);
136 return digestResponse;
139 public Authorization PreAuthenticate(WebRequest webRequest, ICredentials credentials) {
140 GlobalLog.Print("DigestClient::PreAuthenticate() webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()");
141 return DoAuthenticate(null, webRequest, credentials, true);
144 public bool CanPreAuthenticate {
150 public string AuthenticationType {
156 internal static bool CheckQOP(HttpDigestChallenge challenge) {
157 // our internal implementatoin only support "auth" QualityOfProtection.
158 // if it's not what the server wants we'll have to throw:
159 // the case in which the server sends no qop directive we default to "auth"
160 if (challenge.QopPresent) {
163 // find the next occurence of "auth"
164 index = challenge.QualityOfProtection.IndexOf(HttpDigest.SupportedQuality, index);
168 // if it's a whole word we're done
169 if ((index==0 || HttpDigest.ValidSeparator.IndexOf(challenge.QualityOfProtection[index - 1])>=0) &&
170 (index+HttpDigest.SupportedQuality.Length==challenge.QualityOfProtection.Length || HttpDigest.ValidSeparator.IndexOf(challenge.QualityOfProtection[index + HttpDigest.SupportedQuality.Length])>=0) ) {
173 index += HttpDigest.SupportedQuality.Length;
179 public bool Update(string challenge, WebRequest webRequest) {
180 GlobalLog.Print("DigestClient::Update(): [" + challenge + "]");
181 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
183 GlobalLog.Assert(httpWebRequest!=null, "DigestClient::Update()|httpWebRequest == null");
184 GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "DigestClient::Update()|httpWebRequest.ChallengedUri == null");
186 // make sure WDigest fixes these bugs before we enable this code ("Windows OS"):
187 // 921024 1 WDigest should support MD5, at least for explicit (non-default) credentials.
188 // 762116 2 WDigest should ignore directives that do not have a value
189 // 762115 3 WDigest should allow an application to retrieve the parsed domain directive
190 if (httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this) != null) {
191 return XPUpdate(challenge, httpWebRequest);
194 // here's how we know if the handshake is complete when we get the response back,
195 // (keeping in mind that we need to support stale credentials):
196 // !40X - complete & success
197 // 40X & stale=false - complete & failure
198 // 40X & stale=true - !complete
200 if (httpWebRequest.ResponseStatusCode!=httpWebRequest.CurrentAuthenticationState.StatusCodeMatch) {
201 GlobalLog.Print("DigestClient::Update(): no status code match. returning true");
203 ChannelBinding binding = null;
204 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
206 binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
208 httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, binding);
213 int index = challenge==null ? -1 : AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
215 GlobalLog.Print("DigestClient::Update(): no challenge. returning true");
219 int blobBegin = index + SignatureSize;
220 string incoming = null;
223 // there may be multiple challenges. If the next character after the
224 // package name is not a comma then it is challenge data
226 if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
232 if (index >= 0 && challenge.Length > blobBegin) {
233 incoming = challenge.Substring(blobBegin);
236 HttpDigestChallenge digestChallenge = HttpDigest.Interpret(challenge, index, httpWebRequest);
237 if (digestChallenge==null) {
238 GlobalLog.Print("DigestClient::Update(): not a valid digest challenge. returning true");
242 GlobalLog.Print("DigestClient::Update(): returning digestChallenge.Stale:" + digestChallenge.Stale.ToString());
243 return !digestChallenge.Stale;
246 public bool CanUseDefaultCredentials {
248 return WDigestAvailable;
252 internal static bool WDigestAvailable {
254 return _WDigestAvailable;
258 public void ClearSession(WebRequest webRequest) {
259 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
260 GlobalLog.Assert(httpWebRequest != null, "NtlmClient::ClearSession()|httpWebRequest == null");
261 // when we're using WDigest.dll we need to keep the NTAuthentication instance around, since it's in the
262 // challengeCache so remove the reference in the AuthenticationState to avoid closing it in ClearSession
264 httpWebRequest.CurrentAuthenticationState.SetSecurityContext(null, this);
266 httpWebRequest.CurrentAuthenticationState.ClearSession();
271 // On Windows XP and up, WDigest.dll supports the Digest authentication scheme (in addition to
272 // support for HTTP client sides, it also supports HTTP server side and SASL) through SSPI.
274 private Authorization XPDoAuthenticate(string challenge, HttpWebRequest httpWebRequest, ICredentials credentials, bool preAuthenticate) {
275 GlobalLog.Print("DigestClient::XPDoAuthenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " preAuthenticate:" + preAuthenticate.ToString());
277 NTAuthentication authSession = null;
278 string incoming = null;
281 if (!preAuthenticate) {
282 index = AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
286 authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
287 GlobalLog.Print("DigestClient::XPDoAuthenticate() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession));
288 incoming = RefineDigestChallenge(challenge, index);
292 GlobalLog.Print("DigestClient::XPDoAuthenticate() looking up digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
293 authSession = challengeCache.Lookup(httpWebRequest.ChallengedUri.AbsoluteUri) as NTAuthentication;
294 if (authSession==null) {
298 GlobalLog.Print("DigestClient::XPDoAuthenticate() looking up digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
299 HttpDigestChallenge digestChallenge = challengeCache.Lookup(httpWebRequest.ChallengedUri.AbsoluteUri) as HttpDigestChallenge;
300 if (digestChallenge==null) {
304 GlobalLog.Print("DigestClient::XPDoAuthenticate() retrieved digestChallenge#" + ValidationHelper.HashString(digestChallenge) + " digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
305 digestChallenge = digestChallenge.CopyAndIncrementNonce();
306 digestChallenge.SetFromRequest(httpWebRequest);
307 incoming = digestChallenge.ToBlob();
311 UriComponents uriParts = 0;
312 Uri remoteUri = httpWebRequest.GetRemoteResourceUri();
313 if (httpWebRequest.CurrentMethod.ConnectRequest) {
314 uriParts = UriComponents.HostAndPort;
315 // Use the orriginal request Uri, not the proxy Uri
316 remoteUri = httpWebRequest.RequestUri;
319 uriParts = UriComponents.PathAndQuery;
321 // here we use Address instead of ChallengedUri. This is because the
322 // Digest hash is generated using the uri as it is present on the wire
323 string rawUri = remoteUri.GetParts(uriParts, UriFormat.UriEscaped);
324 GlobalLog.Print("DigestClient::XPDoAuthenticate() rawUri:" + ValidationHelper.ToString(rawUri));
326 if (authSession==null) {
327 NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, Signature);
328 GlobalLog.Print("DigestClient::XPDoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
330 if (NC == null || (!(NC is SystemNetworkCredential) && NC.InternalGetUserName().Length == 0))
335 ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
336 if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
339 SpnToken spn = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest);
340 GlobalLog.Print("NtlmClient::Authenticate() ChallengedSpn:" + ValidationHelper.ToString(spn));
342 ChannelBinding binding = null;
343 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
345 binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
349 new NTAuthentication(
350 NegotiationInfoClass.WDigest,
356 GlobalLog.Print("DigestClient::XPDoAuthenticate() setting SecurityContext for:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " to authSession:" + ValidationHelper.HashString(authSession));
357 httpWebRequest.CurrentAuthenticationState.SetSecurityContext(authSession, this);
360 SecurityStatus statusCode;
361 string clientResponse;
363 GlobalLog.Print("DigestClient::XPDoAuthenticate() incoming:" + ValidationHelper.ToString(incoming));
366 clientResponse = authSession.GetOutgoingDigestBlob(incoming, httpWebRequest.CurrentMethod.Name, rawUri, null, preAuthenticate, true, out statusCode);
368 clientResponse = authSession.GetOutgoingDigestBlob(incoming, httpWebRequest.CurrentMethod.Name, rawUri, null, false, false, out statusCode);
370 if (clientResponse == null)
373 GlobalLog.Print("DigestClient::XPDoAuthenticate() GetOutgoingDigestBlob(" + incoming + ") returns:" + ValidationHelper.ToString(clientResponse));
375 Authorization digestResponse = new Authorization(AuthType + " " + clientResponse, authSession.IsCompleted, string.Empty, authSession.IsMutualAuthFlag);
377 if (!preAuthenticate && httpWebRequest.PreAuthenticate) {
378 // add this to the cache of challenges so we can preauthenticate
379 // use this DCR when avaialble to do this without calling HttpDigest.Interpret():
380 // 762115 3 WDigest should allow an application to retrieve the parsed domain directive
381 HttpDigestChallenge digestChallenge = HttpDigest.Interpret(incoming, -1, httpWebRequest);
383 string[] prefixes = digestChallenge.Domain==null ?
384 new string[]{httpWebRequest.ChallengedUri.GetParts(UriComponents.SchemeAndServer, UriFormat.UriEscaped)}
385 : digestChallenge.Domain.Split(singleSpaceArray);
387 // If Domain property is not set we associate Digest only with the server Uri namespace (used to do with whole server)
388 digestResponse.ProtectionRealm = digestChallenge.Domain==null ? null: prefixes;
390 for (int i=0; i<prefixes.Length; i++) {
391 GlobalLog.Print("DigestClient::XPDoAuthenticate() adding authSession#" + ValidationHelper.HashString(authSession) + " for prefix:" + prefixes[i]);
393 challengeCache.Add(prefixes[i], authSession);
395 challengeCache.Add(prefixes[i], digestChallenge);
399 return digestResponse;
403 private bool XPUpdate(string challenge, HttpWebRequest httpWebRequest) {
404 GlobalLog.Print("DigestClient::XPUpdate(): " + challenge);
406 NTAuthentication authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
407 GlobalLog.Print("DigestClient::XPUpdate() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession));
408 if (authSession==null) {
412 int index = challenge==null ? -1 : AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
414 GlobalLog.Print("DigestClient::XPUpdate(): no challenge. returning true");
416 // Extract the CBT we used and cache it for future requests that want to do preauth
417 httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, authSession.ChannelBinding);
419 ClearSession(httpWebRequest);
423 // here's how we know if the handshake is complete when we get the response back,
424 // (keeping in mind that we need to support stale credentials):
425 // !40X - complete & success
426 // 40X & stale=false - complete & failure
427 // 40X & stale=true - !complete
429 if (httpWebRequest.ResponseStatusCode!=httpWebRequest.CurrentAuthenticationState.StatusCodeMatch) {
430 GlobalLog.Print("DigestClient::XPUpdate(): no status code match. returning true");
432 // Extract the CBT we used and cache it for future requests that want to do preauth
433 httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, authSession.ChannelBinding);
435 ClearSession(httpWebRequest);
439 string incoming = RefineDigestChallenge(challenge, index);
440 GlobalLog.Print("DigestClient::XPDoAuthenticate() incoming:" + ValidationHelper.ToString(incoming));
442 // we should get here only on invalid or stale credentials:
443 SecurityStatus statusCode;
444 string clientResponse = authSession.GetOutgoingDigestBlob(incoming, httpWebRequest.CurrentMethod.Name, null, null, false, true, out statusCode);
445 httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated = authSession.IsMutualAuthFlag;
447 GlobalLog.Print("DigestClient::XPUpdate() GetOutgoingDigestBlob(" + incoming + ") returns:" + ValidationHelper.ToString(clientResponse));
448 GlobalLog.Assert(authSession.IsCompleted, "DigestClient::XPUpdate()|IsCompleted == false");
449 GlobalLog.Print("DigestClient::XPUpdate() GetOutgoingBlob() returns clientResponse:[" + ValidationHelper.ToString(clientResponse) + "] IsCompleted:" + authSession.IsCompleted.ToString());
451 return authSession.IsCompleted;
455 // Extract digest relevant part from a raw server blob
457 private static string RefineDigestChallenge(string challenge, int index)
459 string incoming = null;
461 Debug.Assert(challenge != null);
462 Debug.Assert(index >= 0 && index < challenge.Length);
464 int blobBegin = index + SignatureSize;
467 // there may be multiple challenges. If the next character after the
468 // package name is not a comma then it is challenge data
470 if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
477 if (index >= 0 && challenge.Length > blobBegin) {
478 incoming = challenge.Substring(blobBegin);
482 Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_auth_invalid_challenge, DigestClient.AuthType));
483 return String.Empty; // Error, no valid digest challenge, no further processing required
486 // now make sure there's nothing at the end of the challenge that is not part of the digest challenge
487 // this would happen if I have a Digest challenge followed by another challenge like ",NTLM,Negotiate"
488 // use this DCR when avaialble to do this without parsing:
489 // 762116 2 WDigest should ignore directives that do not have a value
490 int startingPoint = 0;
491 int start = startingPoint;
495 HttpDigestChallenge digestChallenge = new HttpDigestChallenge();
498 index = AuthenticationManager.SplitNoQuotes(incoming, ref offset);
499 GlobalLog.Print("DigestClient::XPDoAuthenticate() SplitNoQuotes() returning index:" + index + " offset:" + offset);
503 name = incoming.Substring(start, offset-start);
505 value = HttpDigest.unquote(incoming.Substring(offset+1));
508 value = HttpDigest.unquote(incoming.Substring(offset+1, index-offset-1));
510 valid = digestChallenge.defineAttribute(name, value);
511 GlobalLog.Print("DigestClient::XPDoAuthenticate() defineAttribute(" + name + ", " + value + ") returns " + valid);
512 if (index<0 || !valid) {
517 GlobalLog.Print("DigestClient::XPDoAuthenticate() start:" + start + " offset:" + offset + " index:" + index + " valid:" + valid + " incoming.Length:" + incoming.Length + " incoming:" + incoming);
518 if ((!valid || offset<0) && start<incoming.Length) {
519 incoming = start > 0 ? incoming.Substring(0, start-1) : ""; // First parameter might have been invalid, leaving start at 0
525 internal class HttpDigestChallenge {
527 // General authentication related information
528 internal string HostName;
529 internal string Realm;
530 internal Uri ChallengedUri;
532 // Digest specific fields
534 internal string Nonce;
535 internal string Opaque;
537 internal string Algorithm;
538 internal string Method;
539 internal string Domain;
540 internal string QualityOfProtection;
541 internal string ClientNonce;
542 internal int NonceCount;
543 internal string Charset;
544 internal string ServiceName;
545 internal string ChannelBinding;
547 internal bool UTF8Charset;
548 internal bool QopPresent;
550 internal MD5CryptoServiceProvider MD5provider = new MD5CryptoServiceProvider();
552 internal void SetFromRequest(HttpWebRequest httpWebRequest) {
553 this.HostName = httpWebRequest.ChallengedUri.Host;
554 this.Method = httpWebRequest.CurrentMethod.Name;
556 if (httpWebRequest.CurrentMethod.ConnectRequest) {
557 // Use the orriginal request Uri, not the proxy Uri
558 this.Uri = httpWebRequest.RequestUri.GetParts(UriComponents.HostAndPort, UriFormat.UriEscaped);
561 // Don't use PathAndQuery, it breaks IIS6
562 // GetParts(Path) doesn't return the initial slash
563 this.Uri = "/" + httpWebRequest.GetRemoteResourceUri().GetParts(UriComponents.Path,
564 UriFormat.UriEscaped);
567 this.ChallengedUri = httpWebRequest.ChallengedUri;
570 internal HttpDigestChallenge CopyAndIncrementNonce() {
571 HttpDigestChallenge challengeCopy = null;
573 challengeCopy = this.MemberwiseClone() as HttpDigestChallenge;
576 challengeCopy.MD5provider = new MD5CryptoServiceProvider();
577 return challengeCopy;
580 public bool defineAttribute(string name, string value) {
581 name = name.Trim().ToLower(CultureInfo.InvariantCulture);
582 if (name.Equals(HttpDigest.DA_algorithm)) {
585 else if (name.Equals(HttpDigest.DA_cnonce)) {
588 else if (name.Equals(HttpDigest.DA_nc)) {
589 NonceCount = Int32.Parse(value, NumberFormatInfo.InvariantInfo);
591 else if (name.Equals(HttpDigest.DA_nonce)) {
594 else if (name.Equals(HttpDigest.DA_opaque)) {
597 else if (name.Equals(HttpDigest.DA_qop)) {
598 QualityOfProtection = value;
599 QopPresent = QualityOfProtection!=null && QualityOfProtection.Length>0;
601 else if (name.Equals(HttpDigest.DA_realm)) {
604 else if (name.Equals(HttpDigest.DA_domain)) {
607 else if (name.Equals(HttpDigest.DA_response)) {
609 else if (name.Equals(HttpDigest.DA_stale)) {
610 Stale = value.ToLower(CultureInfo.InvariantCulture).Equals("true");
612 else if (name.Equals(HttpDigest.DA_uri)) {
615 else if (name.Equals(HttpDigest.DA_charset)) {
618 else if (name.Equals(HttpDigest.DA_cipher)) {
621 else if (name.Equals(HttpDigest.DA_username)) {
626 // the token is not recognized, this usually
627 // happens when there are multiple challenges
634 internal string ToBlob() {
635 StringBuilder stringBuilder = new StringBuilder();
636 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_realm, Realm, true));
637 if (Algorithm!=null) {
638 stringBuilder.Append(",");
639 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_algorithm, Algorithm, true));
642 stringBuilder.Append(",");
643 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_charset, Charset, false));
646 stringBuilder.Append(",");
647 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_nonce, Nonce, true));
650 stringBuilder.Append(",");
651 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_uri, Uri, true));
653 if (ClientNonce!=null) {
654 stringBuilder.Append(",");
655 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_cnonce, ClientNonce, true));
658 stringBuilder.Append(",");
659 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_nc, NonceCount.ToString("x8", NumberFormatInfo.InvariantInfo), true));
661 if (QualityOfProtection!=null) {
662 stringBuilder.Append(",");
663 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_qop, QualityOfProtection, true));
666 stringBuilder.Append(",");
667 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_opaque, Opaque, true));
670 stringBuilder.Append(",");
671 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_domain, Domain, true));
674 stringBuilder.Append(",");
675 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_stale, "true", true));
677 return stringBuilder.ToString();
683 internal static class HttpDigest {
685 // these are the tokens defined by Digest
686 // http://www.ietf.org/rfc/rfc2831.txt
688 internal const string DA_algorithm = "algorithm";
689 internal const string DA_cnonce = "cnonce"; // client-nonce
690 internal const string DA_domain = "domain";
691 internal const string DA_nc = "nc"; // nonce-count
692 internal const string DA_nonce = "nonce";
693 internal const string DA_opaque = "opaque";
694 internal const string DA_qop = "qop"; // quality-of-protection
695 internal const string DA_realm = "realm";
696 internal const string DA_response = "response";
697 internal const string DA_stale = "stale";
698 internal const string DA_uri = "uri";
699 internal const string DA_username = "username";
700 internal const string DA_charset = "charset";
701 internal const string DA_cipher = "cipher";
703 // directives specific to CBT. hashed-dirs contains a comma-separated list of directives
704 // that have been hashed into the client nonce. service-name contains the client-provided
705 // SPN. channel-binding contains the hex-encoded MD5 hash of the channel binding token.
706 internal const string DA_hasheddirs = "hashed-dirs";
707 internal const string DA_servicename = "service-name";
708 internal const string DA_channelbinding = "channel-binding";
710 internal const string SupportedQuality = "auth";
711 internal const string ValidSeparator = ", \"\'\t\r\n";
713 // The value of the hashed-dirs directive. It's always "service-name,channel-binding".
714 internal const string HashedDirs = DA_servicename + "," + DA_channelbinding;
716 // A server which understands CBT will send a nonce with this prefix. If we see it,
717 // send a response containing the CBT directives.
718 internal const string Upgraded = "+Upgraded+";
720 // When sending an upgraded response, we prefix this string to the client-nonce directive
721 // to let the server know.
722 internal const string UpgradedV1 = Upgraded + "v1";
724 // If the client application doesn't provide a ChannelBinding, this is what we send
725 // as the channel-binding directive, meaning that the client had no outer secure channel.
726 // See ZeroBindHash in ds/security/protocols/sspcommon/sspbindings.cxx
727 internal const string ZeroChannelBindingHash = "00000000000000000000000000000000";
729 private const string suppressExtendedProtectionKey = @"System\CurrentControlSet\Control\Lsa";
730 private const string suppressExtendedProtectionKeyPath = @"HKEY_LOCAL_MACHINE\" + suppressExtendedProtectionKey;
731 private const string suppressExtendedProtectionValueName = "SuppressExtendedProtection";
733 private static volatile bool suppressExtendedProtection;
735 static HttpDigest() {
736 ReadSuppressExtendedProtectionRegistryValue();
739 [RegistryPermission(SecurityAction.Assert, Read = suppressExtendedProtectionKeyPath)]
740 private static void ReadSuppressExtendedProtectionRegistryValue() {
742 // In Win7 and later, the default value for SuppressExtendedProtection is 0 (enable
743 // CBT support), whereas in pre-Win7 OS versions it is 1 (suppress CBT support).
744 suppressExtendedProtection = !ComNetOS.IsWin7orLater;
747 using (RegistryKey lsaKey = Registry.LocalMachine.OpenSubKey(suppressExtendedProtectionKey)) {
751 // We only consider value 1 (2 is only used for Kerberos and 0 means CBT should
752 // be supported). We ignore all other values.
753 if (lsaKey.GetValueKind(suppressExtendedProtectionValueName) == RegistryValueKind.DWord) {
754 suppressExtendedProtection = ((int)lsaKey.GetValue(suppressExtendedProtectionValueName)) == 1;
757 catch (UnauthorizedAccessException e) {
758 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
760 catch (IOException e) {
761 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
765 catch (SecurityException e) {
766 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
768 catch (ObjectDisposedException e) {
769 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
774 // consider internally caching the nonces sent to us by a server so that
775 // we can correctly send out nonce counts for subsequent requests
778 // used to create a random nonce
780 private static readonly RNGCryptoServiceProvider RandomGenerator = new RNGCryptoServiceProvider();
782 // this method parses the challenge and breaks it into the
783 // fundamental pieces that Digest defines and understands
785 internal static HttpDigestChallenge Interpret(string challenge, int startingPoint, HttpWebRequest httpWebRequest) {
786 HttpDigestChallenge digestChallenge = new HttpDigestChallenge();
787 digestChallenge.SetFromRequest(httpWebRequest);
789 // define the part of the challenge we really care about
791 startingPoint = startingPoint==-1 ? 0 : startingPoint + DigestClient.SignatureSize;
794 int start, offset, index;
797 // forst time parse looking for a charset="utf-8" directive
798 // not too bad, IIS 6.0, by default, sends this as the first directive.
799 // if the server does not send this we'll end up parsing twice.
800 start = startingPoint;
803 index = AuthenticationManager.SplitNoQuotes(challenge, ref offset);
807 name = challenge.Substring(start, offset-start);
808 if (string.Compare(name, DA_charset, StringComparison.OrdinalIgnoreCase)==0) {
810 value = unquote(challenge.Substring(offset+1));
813 value = unquote(challenge.Substring(offset+1, index-offset-1));
815 GlobalLog.Print("HttpDigest::Interpret() server provided a hint to use [" + value + "] encoding");
816 if (string.Compare(value, "utf-8", StringComparison.OrdinalIgnoreCase)==0) {
817 digestChallenge.UTF8Charset = true;
827 // this time go through the directives, parse them and call defineAttribute()
828 start = startingPoint;
831 index = AuthenticationManager.SplitNoQuotes(challenge, ref offset);
832 GlobalLog.Print("HttpDigest::Interpret() SplitNoQuotes() returning index:" + index.ToString() + " offset:" + offset.ToString());
836 name = challenge.Substring(start, offset-start);
838 value = unquote(challenge.Substring(offset+1));
841 value = unquote(challenge.Substring(offset+1, index-offset-1));
843 if (digestChallenge.UTF8Charset) {
845 for (int i=0; i<value.Length; i++) {
846 if (value[i]>(char)0x7F) {
852 GlobalLog.Print("HttpDigest::Interpret() UTF8 decoding required value:[" + value + "]");
853 byte[] bytes = new byte[value.Length];
854 for (int i=0; i<value.Length; i++) {
855 bytes[i] = (byte)value[i];
857 value = Encoding.UTF8.GetString(bytes);
858 GlobalLog.Print("HttpDigest::Interpret() UTF8 decoded value:[" + value + "]");
861 GlobalLog.Print("HttpDigest::Interpret() no need for special encoding");
864 valid = digestChallenge.defineAttribute(name, value);
865 GlobalLog.Print("HttpDigest::Interpret() defineAttribute(" + name + ", " + value + ") returns " + valid.ToString());
866 if (index<0 || !valid) {
871 // We must absolutely have a nonce for Digest to work.
872 if (digestChallenge.Nonce == null) {
874 Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_requires_nonce));
878 return digestChallenge;
881 private enum Charset {
887 private static string CharsetEncode(string rawString, Charset charset) {
889 GlobalLog.Print("HttpDigest::CharsetEncode() encoding rawString:[" + rawString + "] Chars(rawString):[" + Chars(rawString) + "] charset:[" + charset + "]");
891 if (charset==Charset.UTF8 || charset==Charset.ANSI) {
892 byte[] bytes = charset==Charset.UTF8 ? Encoding.UTF8.GetBytes(rawString) : Encoding.Default.GetBytes(rawString);
893 // the following code is the same as:
894 // rawString = Encoding.Default.GetString(bytes);
896 char[] chars = new char[bytes.Length];
897 bytes.CopyTo(chars, 0);
898 rawString = new string(chars);
901 GlobalLog.Print("HttpDigest::CharsetEncode() encoded rawString:[" + rawString + "] Chars(rawString):[" + Chars(rawString) + "] charset:[" + charset + "]");
906 private static Charset DetectCharset(string rawString) {
907 Charset charset = Charset.ASCII;
908 for (int i=0; i<rawString.Length; i++) {
909 if (rawString[i]>(char)0x7F) {
910 GlobalLog.Print("HttpDigest::DetectCharset() found non ASCII character:[" + ((int)rawString[i]).ToString() + "] at offset i:[" + i.ToString() + "] charset:[" + charset.ToString() + "]");
911 // ----, but the only way we can tell if we can use default ANSI encoding is see
912 // in the encode/decode process there is no loss of information.
913 byte[] bytes = Encoding.Default.GetBytes(rawString);
914 string rawCopy = Encoding.Default.GetString(bytes);
915 charset = string.Compare(rawString, rawCopy, StringComparison.Ordinal)==0 ? Charset.ANSI : Charset.UTF8;
919 GlobalLog.Print("HttpDigest::DetectCharset() rawString:[" + rawString + "] has charset:[" + charset.ToString() + "]");
924 private static string Chars(string rawString) {
925 string returnString = "[";
926 for (int i=0; i<rawString.Length; i++) {
930 returnString += ((int)rawString[i]).ToString();
932 return returnString + "]";
938 // creating a static hashtable for server nonces and keep track of nonce count
940 internal static Authorization Authenticate(HttpDigestChallenge digestChallenge, NetworkCredential NC, string spn, ChannelBinding binding) {
942 string username = NC.InternalGetUserName();
943 if (ValidationHelper.IsBlankString(username)) {
946 string password = NC.InternalGetPassword();
948 bool upgraded = IsUpgraded(digestChallenge.Nonce, binding);
951 digestChallenge.ServiceName = spn;
952 digestChallenge.ChannelBinding = hashChannelBinding(binding, digestChallenge.MD5provider);
955 if (digestChallenge.QopPresent) {
956 if (digestChallenge.ClientNonce==null || digestChallenge.Stale) {
957 GlobalLog.Print("HttpDigest::Authenticate() QopPresent:True, need new nonce. digestChallenge.ClientNonce:" + ValidationHelper.ToString(digestChallenge.ClientNonce) + " digestChallenge.Stale:" + digestChallenge.Stale.ToString());
961 digestChallenge.ClientNonce = createUpgradedNonce(digestChallenge);
965 digestChallenge.ClientNonce = createNonce(32);
968 digestChallenge.NonceCount = 1;
971 GlobalLog.Print("HttpDigest::Authenticate() QopPresent:True, reusing nonce. digestChallenge.NonceCount:" + digestChallenge.NonceCount.ToString());
972 digestChallenge.NonceCount++;
976 StringBuilder authorization = new StringBuilder();
979 // look at username & password, if it's not ASCII we need to attempt some
980 // kind of encoding because we need to calculate the hash on byte[]
982 Charset usernameCharset = DetectCharset(username);
983 if (!digestChallenge.UTF8Charset && usernameCharset==Charset.UTF8) {
984 GlobalLog.Print("HttpDigest::Authenticate() can't authenticate with UNICODE username. failing auth.");
987 Charset passwordCharset = DetectCharset(password);
988 if (!digestChallenge.UTF8Charset && passwordCharset==Charset.UTF8) {
989 GlobalLog.Print("HttpDigest::Authenticate() can't authenticate with UNICODE password. failing auth.");
992 if (digestChallenge.UTF8Charset) {
993 // on the wire always use UTF8 when the server supports it
994 authorization.Append(pair(DA_charset, "utf-8", false));
995 authorization.Append(",");
996 if (usernameCharset==Charset.UTF8) {
997 username = CharsetEncode(username, Charset.UTF8);
998 authorization.Append(pair(DA_username, username, true));
999 authorization.Append(",");
1002 authorization.Append(pair(DA_username, CharsetEncode(username, Charset.UTF8), true));
1003 authorization.Append(",");
1004 username = CharsetEncode(username, usernameCharset);
1008 // otherwise UTF8 is not required
1009 username = CharsetEncode(username, usernameCharset);
1010 authorization.Append(pair(DA_username, username, true));
1011 authorization.Append(",");
1014 password = CharsetEncode(password, passwordCharset);
1016 // no special encoding for the realm since we're just going to echo it back (encoding must have happened on the server).
1017 authorization.Append(pair(DA_realm, digestChallenge.Realm, true));
1018 authorization.Append(",");
1019 authorization.Append(pair(DA_nonce, digestChallenge.Nonce, true));
1020 authorization.Append(",");
1021 authorization.Append(pair(DA_uri, digestChallenge.Uri, true));
1023 if (digestChallenge.QopPresent) {
1024 if (digestChallenge.Algorithm!=null) {
1025 // consider: should we default to "MD5" here? IE does
1026 authorization.Append(",");
1027 authorization.Append(pair(DA_algorithm, digestChallenge.Algorithm, true)); // IE sends quotes - IIS needs them
1029 authorization.Append(",");
1030 authorization.Append(pair(DA_cnonce, digestChallenge.ClientNonce, true));
1031 authorization.Append(",");
1032 authorization.Append(pair(DA_nc, digestChallenge.NonceCount.ToString("x8", NumberFormatInfo.InvariantInfo), false));
1034 // send only the QualityOfProtection we're using
1035 // since we support only "auth" that's what we will send out
1036 authorization.Append(",");
1037 authorization.Append(pair(DA_qop, SupportedQuality, true)); // IE sends quotes - IIS needs them
1041 authorization.Append(",");
1042 authorization.Append(pair(DA_hasheddirs, HashedDirs, true));
1043 authorization.Append(",");
1044 authorization.Append(pair(DA_servicename, digestChallenge.ServiceName, true));
1045 authorization.Append(",");
1046 authorization.Append(pair(DA_channelbinding, digestChallenge.ChannelBinding, true));
1050 // warning: this must be computed here
1051 string responseValue = HttpDigest.responseValue(digestChallenge, username, password);
1052 if (responseValue==null) {
1056 authorization.Append(",");
1057 authorization.Append(pair(DA_response, responseValue, true)); // IE sends quotes - IIS needs them
1059 if (digestChallenge.Opaque!=null) {
1060 authorization.Append(",");
1061 authorization.Append(pair(DA_opaque, digestChallenge.Opaque, true));
1064 GlobalLog.Print("HttpDigest::Authenticate() digestChallenge.Stale:" + digestChallenge.Stale.ToString());
1066 // completion is decided in Update()
1067 Authorization finalAuthorization = new Authorization(DigestClient.AuthType + " " + authorization.ToString(), false);
1069 return finalAuthorization;
1072 private static bool IsUpgraded(string nonce, ChannelBinding binding) {
1074 GlobalLog.Assert(nonce != null, "HttpDigest::IsUpgraded()|'nonce' must not be null.");
1076 // Digest-SSP ignores the SuppressExtendedProtection Registry value, if the the caller
1077 // passes a channel binding. I.e. we must consider SuppressExtendedProtection only if
1078 // there is no channel binding (e.g. in the http:// case).
1079 if ((binding == null) && (suppressExtendedProtection)) {
1083 // Extended Protection is only possible if both the SSPs on the current system support
1084 // EP and the server sent a 'nonce' containing the +Upgraded+ prefix.
1085 return AuthenticationManager.SspSupportsExtendedProtection &&
1086 nonce.StartsWith(Upgraded, StringComparison.Ordinal);
1089 internal static string unquote(string quotedString) {
1090 return quotedString.Trim().Trim("\"".ToCharArray());
1093 // Returns the string consisting of <name> followed by
1094 // an equal sign, followed by the <value> in double-quotes
1095 internal static string pair(string name, string value, bool quote) {
1097 return name + "=\"" + value + "\"";
1099 return name + "=" + value;
1103 // this method computes the response-value according to the
1104 // rules described in RFC2831 section 2.1.2.1
1106 private static string responseValue(HttpDigestChallenge challenge, string username, string password) {
1107 string secretString = computeSecret(challenge, username, password);
1108 if (secretString == null) {
1112 // we assume auth here, since it's the only one we support, the check happened earlier
1113 string dataString = challenge.Method + ":" + challenge.Uri;
1114 if (dataString == null) {
1118 string secret = hashString(secretString, challenge.MD5provider);
1119 string hexMD2 = hashString(dataString, challenge.MD5provider);
1122 challenge.Nonce + ":" +
1123 (challenge.QopPresent ?
1124 challenge.NonceCount.ToString("x8", NumberFormatInfo.InvariantInfo) + ":" +
1125 challenge.ClientNonce + ":" +
1126 SupportedQuality + ":" + // challenge.QualityOfProtection + ":" +
1131 return hashString(secret + ":" + data, challenge.MD5provider);
1134 private static string computeSecret(HttpDigestChallenge challenge, string username, string password) {
1135 if (challenge.Algorithm==null || string.Compare(challenge.Algorithm, "md5" ,StringComparison.OrdinalIgnoreCase)==0) {
1136 return username + ":" + challenge.Realm + ":" + password;
1138 else if (string.Compare(challenge.Algorithm, "md5-sess" ,StringComparison.OrdinalIgnoreCase)==0) {
1139 return hashString(username + ":" + challenge.Realm + ":" + password, challenge.MD5provider) + ":" + challenge.Nonce + ":" + challenge.ClientNonce;
1142 Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_hash_algorithm_not_supported, challenge.Algorithm));
1146 // Where in the SecChannelBindings struct to find these fields
1147 private static int InitiatorTypeOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "dwInitiatorAddrType");
1148 private static int InitiatorLengthOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "cbInitiatorLength");
1149 private static int InitiatorOffsetOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "dwInitiatorOffset");
1150 private static int AcceptorTypeOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "dwAcceptorAddrType");
1151 private static int AcceptorLengthOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "cbAcceptorLength");
1152 private static int AcceptorOffsetOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "dwAcceptorOffset");
1153 private static int ApplicationDataLengthOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "cbApplicationDataLength");
1154 private static int ApplicationDataOffsetOffset = (int)Marshal.OffsetOf(typeof(SecChannelBindings), "dwApplicationDataOffset");
1156 private static int SizeOfInt = Marshal.SizeOf(typeof(int));
1157 private static int MinimumFormattedBindingLength = 5 * SizeOfInt;
1160 // Adapted from ComputeGssBindHash() in ds\security\protocols\sspcommon\sspbindings.cxx
1162 // The formatted binding is:
1163 // 1. the initiator type and length
1164 // 2. the initiator data, if any
1165 // 3. the acceptor type and length
1166 // 4. the acceptor data, if any
1167 // 5. the application data length
1168 // 6. the application data, if any
1170 private static byte[] formatChannelBindingForHash(ChannelBinding binding)
1172 int initiatorType = Marshal.ReadInt32(binding.DangerousGetHandle(), InitiatorTypeOffset);
1173 int initiatorLength = Marshal.ReadInt32(binding.DangerousGetHandle(), InitiatorLengthOffset);
1174 int acceptorType = Marshal.ReadInt32(binding.DangerousGetHandle(), AcceptorTypeOffset);
1175 int acceptorLength = Marshal.ReadInt32(binding.DangerousGetHandle(), AcceptorLengthOffset);
1176 int applicationDataLength = Marshal.ReadInt32(binding.DangerousGetHandle(), ApplicationDataLengthOffset);
1178 byte[] formattedData = new byte[MinimumFormattedBindingLength + initiatorLength + acceptorLength + applicationDataLength];
1180 BitConverter.GetBytes(initiatorType).CopyTo(formattedData, 0);
1181 BitConverter.GetBytes(initiatorLength).CopyTo(formattedData, SizeOfInt);
1183 int offset = 2 * SizeOfInt;
1184 if (initiatorLength > 0)
1186 int initiatorOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), InitiatorOffsetOffset);
1187 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), initiatorOffset), formattedData, offset, initiatorLength);
1188 offset += initiatorLength;
1191 BitConverter.GetBytes(acceptorType).CopyTo(formattedData, offset);
1192 BitConverter.GetBytes(acceptorLength).CopyTo(formattedData, offset + SizeOfInt);
1194 offset += 2 * SizeOfInt;
1195 if (acceptorLength > 0)
1197 int acceptorOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), AcceptorOffsetOffset);
1198 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), acceptorOffset), formattedData, offset, acceptorLength);
1199 offset += acceptorLength;
1202 BitConverter.GetBytes(applicationDataLength).CopyTo(formattedData, offset);
1204 offset += SizeOfInt;
1205 if (applicationDataLength > 0)
1207 int applicationDataOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), ApplicationDataOffsetOffset);
1208 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), applicationDataOffset), formattedData, offset, applicationDataLength);
1211 return formattedData;
1214 private static string hashChannelBinding(ChannelBinding binding, MD5CryptoServiceProvider MD5provider)
1216 if (binding == null)
1218 return ZeroChannelBindingHash;
1221 byte[] formattedData = formatChannelBindingForHash(binding);
1222 byte[] hash = MD5provider.ComputeHash(formattedData);
1224 return hexEncode(hash);
1227 private static string hashString(string myString, MD5CryptoServiceProvider MD5provider) {
1228 GlobalLog.Enter("HttpDigest::hashString", "[" + myString.Length.ToString() + ":" + myString + "]");
1229 byte[] encodedBytes = new byte[myString.Length];
1230 for (int i=0; i<myString.Length; i++) {
1231 encodedBytes[i] = (byte)myString[i];
1233 byte[] hash = MD5provider.ComputeHash(encodedBytes);
1234 string hashString = hexEncode(hash);
1235 GlobalLog.Leave("HttpDigest::hashString", "[" + hashString.Length.ToString() + ":" + hashString + "]");
1239 private static string hexEncode(byte[] rawbytes) {
1240 int size = rawbytes.Length;
1241 char[] wa = new char[2*size];
1243 for (int i=0, dp=0; i<size; i++) {
1244 // warning: these ARE case sensitive
1245 wa[dp++] = Uri.HexLowerChars[rawbytes[i]>>4];
1246 wa[dp++] = Uri.HexLowerChars[rawbytes[i]&0x0F];
1249 return new string(wa);
1252 /* returns a random nonce of given length */
1253 private static string createNonce(int length) {
1254 // we'd need less (half of that), but this makes the code much simpler
1255 int bytesNeeded = length;
1256 byte[] randomBytes = new byte[bytesNeeded];
1257 char[] digits = new char[length];
1258 RandomGenerator.GetBytes(randomBytes);
1259 for (int i=0; i<length; i++) {
1260 // warning: these ARE case sensitive
1261 digits[i] = Uri.HexLowerChars[randomBytes[i]&0x0F];
1263 return new string(digits);
1266 private static string createUpgradedNonce(HttpDigestChallenge digestChallenge)
1268 string hashMe = digestChallenge.ServiceName + ":" + digestChallenge.ChannelBinding;
1269 byte[] hash = digestChallenge.MD5provider.ComputeHash(Encoding.ASCII.GetBytes(hashMe));
1271 return UpgradedV1 + hexEncode(hash) + createNonce(32);