Update Reference Sources to .NET Framework 4.6.1
[mono.git] / mcs / class / referencesource / System / net / System / Net / _DigestClient.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="_DigestClient.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6
7 namespace System.Net {
8     using System.Net.Sockets;
9     using System.Collections;
10     using System.Collections.Generic;
11     using System.Text;
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;
18     using System.IO;
19     using System.Security;
20     using System.Diagnostics;
21
22     internal class DigestClient : ISessionAuthenticationModule {
23
24         internal const string AuthType = "Digest";
25         internal static string Signature = AuthType.ToLower(CultureInfo.InvariantCulture);
26         internal static int SignatureSize = Signature.Length;
27
28         private static PrefixLookup challengeCache = new PrefixLookup();
29         private static readonly char[] singleSpaceArray = new char[]{' '};
30
31         // [....]: make sure WDigest fixes these bugs before we
32         // enable this code ("Windows OS" Product Studio database):
33         //
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
37         //
38         private static bool _WDigestAvailable;
39
40         static DigestClient() {
41             _WDigestAvailable = SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, NegotiationInfoClass.WDigest)!=null;
42         }
43
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);
47         }
48
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());
51
52             GlobalLog.Assert(credentials != null, "DigestClient::DoAuthenticate()|credentials == null");
53             if (credentials==null) {
54                 return null;
55             }
56
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");
60
61             // If it's default credentials, we support them on XP and up through WDigest.
62
63
64             NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, DigestClient.Signature);
65             GlobalLog.Print("DigestClient::DoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
66
67             if (NC is SystemNetworkCredential) {
68                 if (WDigestAvailable) {
69                     return XPDoAuthenticate(challenge, httpWebRequest, credentials, preAuthenticate);
70                 }
71                 else {
72                     return null;
73                 }
74             }
75
76             HttpDigestChallenge digestChallenge;
77             if (!preAuthenticate) {
78                 int index = AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
79                 if (index < 0) {
80                     return null;
81                 }
82                 digestChallenge = HttpDigest.Interpret(challenge, index, httpWebRequest);
83             }
84             else {
85                 GlobalLog.Print("DigestClient::DoAuthenticate() looking up digestChallenge for prefix:" + httpWebRequest.ChallengedUri.AbsoluteUri);
86                 digestChallenge = challengeCache.Lookup(httpWebRequest.ChallengedUri.AbsoluteUri) as HttpDigestChallenge;
87             }
88             if (digestChallenge==null) {
89                 return null;
90             }
91
92             bool supported = CheckQOP(digestChallenge);
93             if (!supported) {
94                 if (Logging.On)
95                     Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_qop_not_supported, digestChallenge.QualityOfProtection));
96                 return null;
97             }
98
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);
103             }
104
105             if (NC==null) {
106                 return null;
107             }
108
109             ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
110             if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
111                 return null;
112
113             SpnToken spnToken = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest);
114
115             ChannelBinding binding = null;
116             if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
117             {
118                 binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
119             }
120
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);
127
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;
130
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);
134                 }
135             }
136             return digestResponse;
137         }
138
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);
142         }
143
144         public bool CanPreAuthenticate {
145             get {
146                 return true;
147             }
148         }
149
150         public string AuthenticationType {
151             get {
152                 return AuthType;
153             }
154         }
155
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) {
161                 int index = 0;
162                 while (index>=0) {
163                     // find the next occurence of "auth"
164                     index = challenge.QualityOfProtection.IndexOf(HttpDigest.SupportedQuality, index);
165                     if (index<0) {
166                         return false;
167                     }
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) ) {
171                         break;
172                     }
173                     index += HttpDigest.SupportedQuality.Length;
174                 }
175             }
176             return true;
177         }
178
179         public bool Update(string challenge, WebRequest webRequest) {
180             GlobalLog.Print("DigestClient::Update(): [" + challenge + "]");
181             HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
182
183             GlobalLog.Assert(httpWebRequest!=null, "DigestClient::Update()|httpWebRequest == null");
184             GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "DigestClient::Update()|httpWebRequest.ChallengedUri == null");
185
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);
192             }
193
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
199
200             if (httpWebRequest.ResponseStatusCode!=httpWebRequest.CurrentAuthenticationState.StatusCodeMatch) {
201                 GlobalLog.Print("DigestClient::Update(): no status code match. returning true");
202
203                 ChannelBinding binding = null;
204                 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
205                 {
206                     binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
207                 }
208                 httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, binding);
209
210                 return true;
211             }
212
213             int index = challenge==null ? -1 : AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
214             if (index < 0) {
215                 GlobalLog.Print("DigestClient::Update(): no challenge. returning true");
216                 return true;
217             }
218
219             int blobBegin = index + SignatureSize;
220             string incoming = null;
221
222             //
223             // there may be multiple challenges. If the next character after the
224             // package name is not a comma then it is challenge data
225             //
226             if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
227                 ++blobBegin;
228             }
229             else {
230                 index = -1;
231             }
232             if (index >= 0 && challenge.Length > blobBegin) {
233                 incoming = challenge.Substring(blobBegin);
234             }
235
236             HttpDigestChallenge digestChallenge = HttpDigest.Interpret(challenge, index, httpWebRequest);
237             if (digestChallenge==null) {
238                 GlobalLog.Print("DigestClient::Update(): not a valid digest challenge. returning true");
239                 return true;
240             }
241
242             GlobalLog.Print("DigestClient::Update(): returning digestChallenge.Stale:" + digestChallenge.Stale.ToString());
243             return !digestChallenge.Stale;
244         }
245
246         public bool CanUseDefaultCredentials {
247             get {
248                 return WDigestAvailable;
249             }
250         }
251
252         internal static bool WDigestAvailable {
253             get {
254                 return _WDigestAvailable;
255             }
256         }
257
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
263 #if WDIGEST_PREAUTH
264             httpWebRequest.CurrentAuthenticationState.SetSecurityContext(null, this);
265 #else
266             httpWebRequest.CurrentAuthenticationState.ClearSession();
267 #endif
268         }
269
270
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.
273
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());
276
277             NTAuthentication authSession = null;
278             string incoming = null;
279             int index;
280
281             if (!preAuthenticate) {
282                 index = AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
283                 if (index < 0) {
284                     return null;
285                 }
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);
289             }
290             else {
291 #if WDIGEST_PREAUTH
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) {
295                     return null;
296                 }
297 #else
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) {
301                     return null;
302                 }
303
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();
308 #endif
309             }
310
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;
317             }
318             else {
319                 uriParts = UriComponents.PathAndQuery;
320             }
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));
325
326             if (authSession==null) {
327                 NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, Signature);
328                 GlobalLog.Print("DigestClient::XPDoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
329
330                 if (NC == null || (!(NC is SystemNetworkCredential) && NC.InternalGetUserName().Length == 0))
331                 {
332                     return null;
333                 }
334
335                 ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
336                 if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
337                     return null;
338
339                 SpnToken spn = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest);
340                 GlobalLog.Print("NtlmClient::Authenticate() ChallengedSpn:" + ValidationHelper.ToString(spn));
341
342                 ChannelBinding binding = null;
343                 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
344                 {
345                     binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
346                 }
347
348                 authSession =
349                     new NTAuthentication(
350                         NegotiationInfoClass.WDigest,
351                         NC,
352                         spn,
353                         httpWebRequest,
354                         binding);
355
356                 GlobalLog.Print("DigestClient::XPDoAuthenticate() setting SecurityContext for:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " to authSession:" + ValidationHelper.HashString(authSession));
357                 httpWebRequest.CurrentAuthenticationState.SetSecurityContext(authSession, this);
358             }
359
360             SecurityStatus statusCode;
361             string clientResponse;
362
363             GlobalLog.Print("DigestClient::XPDoAuthenticate() incoming:" + ValidationHelper.ToString(incoming));
364
365 #if WDIGEST_PREAUTH
366             clientResponse = authSession.GetOutgoingDigestBlob(incoming, httpWebRequest.CurrentMethod.Name, rawUri, null, preAuthenticate, true, out statusCode);
367 #else
368             clientResponse = authSession.GetOutgoingDigestBlob(incoming, httpWebRequest.CurrentMethod.Name, rawUri, null, false, false, out statusCode);
369 #endif
370             if (clientResponse == null)
371                 return null;
372
373             GlobalLog.Print("DigestClient::XPDoAuthenticate() GetOutgoingDigestBlob(" + incoming + ") returns:" + ValidationHelper.ToString(clientResponse));
374
375             Authorization digestResponse = new Authorization(AuthType + " " + clientResponse, authSession.IsCompleted, string.Empty, authSession.IsMutualAuthFlag);
376
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);
382
383                 string[] prefixes = digestChallenge.Domain==null ?
384                         new string[]{httpWebRequest.ChallengedUri.GetParts(UriComponents.SchemeAndServer, UriFormat.UriEscaped)}
385                         : digestChallenge.Domain.Split(singleSpaceArray);
386
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;
389
390                 for (int i=0; i<prefixes.Length; i++) {
391                     GlobalLog.Print("DigestClient::XPDoAuthenticate() adding authSession#" + ValidationHelper.HashString(authSession) + " for prefix:" + prefixes[i]);
392 #if WDIGEST_PREAUTH
393                     challengeCache.Add(prefixes[i], authSession);
394 #else
395                     challengeCache.Add(prefixes[i], digestChallenge);
396 #endif
397                 }
398             }
399             return digestResponse;
400         }
401
402
403         private bool XPUpdate(string challenge, HttpWebRequest httpWebRequest) {
404             GlobalLog.Print("DigestClient::XPUpdate(): " + challenge);
405
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) {
409                 return false;
410             }
411
412             int index = challenge==null ? -1 : AuthenticationManager.FindSubstringNotInQuotes(challenge, Signature);
413             if (index < 0) {
414                 GlobalLog.Print("DigestClient::XPUpdate(): no challenge. returning true");
415
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);
418
419                 ClearSession(httpWebRequest);
420                 return true;
421             }
422
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
428
429             if (httpWebRequest.ResponseStatusCode!=httpWebRequest.CurrentAuthenticationState.StatusCodeMatch) {
430                 GlobalLog.Print("DigestClient::XPUpdate(): no status code match. returning true");
431
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);
434
435                 ClearSession(httpWebRequest);
436                 return true;
437             }
438
439             string incoming = RefineDigestChallenge(challenge, index);
440             GlobalLog.Print("DigestClient::XPDoAuthenticate() incoming:" + ValidationHelper.ToString(incoming));
441
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;
446
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());
450
451             return authSession.IsCompleted;
452         }
453
454         //
455         // Extract digest relevant part from a raw server blob
456         //
457         private static string RefineDigestChallenge(string challenge, int index)
458         {
459             string incoming = null;
460
461             Debug.Assert(challenge != null);
462             Debug.Assert(index >= 0 && index < challenge.Length);
463
464             int blobBegin = index + SignatureSize;
465
466             //
467             // there may be multiple challenges. If the next character after the
468             // package name is not a comma then it is challenge data
469             //
470             if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
471                 ++blobBegin;
472             }
473             else {
474                 index = -1;
475             }
476
477             if (index >= 0 && challenge.Length > blobBegin) {
478                 incoming = challenge.Substring(blobBegin);
479             }
480             else
481             {
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
484             }
485
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;
492             int offset;
493             bool valid = true;
494             string name, value;
495             HttpDigestChallenge digestChallenge = new HttpDigestChallenge();
496             for (;;) {
497                 offset = start;
498                 index = AuthenticationManager.SplitNoQuotes(incoming, ref offset);
499                 GlobalLog.Print("DigestClient::XPDoAuthenticate() SplitNoQuotes() returning index:" + index + " offset:" + offset);
500                 if (offset<0) {
501                     break;
502                 }
503                 name = incoming.Substring(start, offset-start);
504                 if (index<0) {
505                     value = HttpDigest.unquote(incoming.Substring(offset+1));
506                 }
507                 else {
508                     value = HttpDigest.unquote(incoming.Substring(offset+1, index-offset-1));
509                 }
510                 valid = digestChallenge.defineAttribute(name, value);
511                 GlobalLog.Print("DigestClient::XPDoAuthenticate() defineAttribute(" + name + ", " + value + ") returns " + valid);
512                 if (index<0 || !valid) {
513                     break;
514                 }
515                 start = ++index;
516             }
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
520             }
521             return incoming;
522         }
523     }
524
525     internal class HttpDigestChallenge {
526
527         // General authentication related information
528         internal string   HostName;
529         internal string   Realm;
530         internal Uri      ChallengedUri;
531
532         // Digest specific fields
533         internal string   Uri;
534         internal string   Nonce;
535         internal string   Opaque;
536         internal bool     Stale;
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;
546
547         internal bool     UTF8Charset;
548         internal bool     QopPresent;
549
550         internal MD5CryptoServiceProvider MD5provider = new MD5CryptoServiceProvider();
551
552         internal void SetFromRequest(HttpWebRequest httpWebRequest) {
553             this.HostName = httpWebRequest.ChallengedUri.Host;
554             this.Method = httpWebRequest.CurrentMethod.Name;
555
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);
559             }
560             else {
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);
565             }
566
567             this.ChallengedUri = httpWebRequest.ChallengedUri;
568         }
569
570         internal HttpDigestChallenge CopyAndIncrementNonce() {
571             HttpDigestChallenge challengeCopy = null;
572             lock(this) {
573                 challengeCopy = this.MemberwiseClone() as HttpDigestChallenge;
574                 ++NonceCount;
575             }
576             challengeCopy.MD5provider = new MD5CryptoServiceProvider();
577             return challengeCopy;
578         }
579
580         public bool defineAttribute(string name, string value) {
581             name = name.Trim().ToLower(CultureInfo.InvariantCulture);
582             if (name.Equals(HttpDigest.DA_algorithm)) {
583                 Algorithm = value;
584             }
585             else if (name.Equals(HttpDigest.DA_cnonce)) {
586                 ClientNonce = value;
587             }
588             else if (name.Equals(HttpDigest.DA_nc)) {
589                 NonceCount = Int32.Parse(value, NumberFormatInfo.InvariantInfo);
590             }
591             else if (name.Equals(HttpDigest.DA_nonce)) {
592                 Nonce = value;
593             }
594             else if (name.Equals(HttpDigest.DA_opaque)) {
595                 Opaque = value;
596             }
597             else if (name.Equals(HttpDigest.DA_qop)) {
598                 QualityOfProtection = value;
599                 QopPresent = QualityOfProtection!=null && QualityOfProtection.Length>0;
600             }
601             else if (name.Equals(HttpDigest.DA_realm)) {
602                 Realm = value;
603             }
604             else if (name.Equals(HttpDigest.DA_domain)) {
605                 Domain = value;
606             }
607             else if (name.Equals(HttpDigest.DA_response)) {
608             }
609             else if (name.Equals(HttpDigest.DA_stale)) {
610                 Stale = value.ToLower(CultureInfo.InvariantCulture).Equals("true");
611             }
612             else if (name.Equals(HttpDigest.DA_uri)) {
613                 Uri = value;
614             }
615             else if (name.Equals(HttpDigest.DA_charset)) {
616                 Charset = value;
617             }
618             else if (name.Equals(HttpDigest.DA_cipher)) {
619                 // ignore
620             }
621             else if (name.Equals(HttpDigest.DA_username)) {
622                 // ignore
623             }
624             else {
625                 //
626                 // the token is not recognized, this usually
627                 // happens when there are multiple challenges
628                 //
629                 return false;
630             }
631             return true;
632         }
633
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));
640             }
641             if (Charset!=null) {
642                 stringBuilder.Append(",");
643                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_charset, Charset, false));
644             }
645             if (Nonce!=null) {
646                 stringBuilder.Append(",");
647                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_nonce, Nonce, true));
648             }
649             if (Uri!=null) {
650                 stringBuilder.Append(",");
651                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_uri, Uri, true));
652             }
653             if (ClientNonce!=null) {
654                 stringBuilder.Append(",");
655                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_cnonce, ClientNonce, true));
656             }
657             if (NonceCount>0) {
658                 stringBuilder.Append(",");
659                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_nc, NonceCount.ToString("x8", NumberFormatInfo.InvariantInfo), true));
660             }
661             if (QualityOfProtection!=null) {
662                 stringBuilder.Append(",");
663                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_qop, QualityOfProtection, true));
664             }
665             if (Opaque!=null) {
666                 stringBuilder.Append(",");
667                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_opaque, Opaque, true));
668             }
669             if (Domain!=null) {
670                 stringBuilder.Append(",");
671                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_domain, Domain, true));
672             }
673             if (Stale) {
674                 stringBuilder.Append(",");
675                 stringBuilder.Append(HttpDigest.pair(HttpDigest.DA_stale, "true", true));
676             }
677             return stringBuilder.ToString();
678         }
679
680     }
681
682
683     internal static class HttpDigest {
684         //
685         // these are the tokens defined by Digest
686         // http://www.ietf.org/rfc/rfc2831.txt
687         //
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";
702
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";
709
710         internal const string SupportedQuality = "auth";
711         internal const string ValidSeparator = ", \"\'\t\r\n";
712
713         // The value of the hashed-dirs directive.  It's always "service-name,channel-binding".
714         internal const string HashedDirs = DA_servicename + "," + DA_channelbinding;
715
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+";
719
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";
723
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";
728
729         private const string suppressExtendedProtectionKey = @"System\CurrentControlSet\Control\Lsa";
730         private const string suppressExtendedProtectionKeyPath = @"HKEY_LOCAL_MACHINE\" + suppressExtendedProtectionKey;
731         private const string suppressExtendedProtectionValueName = "SuppressExtendedProtection";
732
733         private static volatile bool suppressExtendedProtection;
734
735         static HttpDigest() {
736             ReadSuppressExtendedProtectionRegistryValue();
737         }
738
739         [RegistryPermission(SecurityAction.Assert, Read = suppressExtendedProtectionKeyPath)]
740         private static void ReadSuppressExtendedProtectionRegistryValue() {
741
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;
745
746             try {
747                 using (RegistryKey lsaKey = Registry.LocalMachine.OpenSubKey(suppressExtendedProtectionKey)) {
748
749                     try
750                     {
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;
755                         }
756                     }
757                     catch (UnauthorizedAccessException e) {
758                         if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
759                     }
760                     catch (IOException e) {
761                         if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
762                     }
763                 }
764             }
765             catch (SecurityException e) {
766                 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
767             }
768             catch (ObjectDisposedException e) {
769                 if (Logging.On) Logging.PrintWarning(Logging.Web, typeof(HttpDigest), "ReadSuppressExtendedProtectionRegistryValue", e.Message);
770             }
771         }
772
773         //
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
776
777         //
778         // used to create a random nonce
779         //
780         private static readonly RNGCryptoServiceProvider RandomGenerator = new RNGCryptoServiceProvider();
781         //
782         // this method parses the challenge and breaks it into the
783         // fundamental pieces that Digest defines and understands
784         //
785         internal static HttpDigestChallenge Interpret(string challenge, int startingPoint, HttpWebRequest httpWebRequest) {
786             HttpDigestChallenge digestChallenge = new HttpDigestChallenge();
787             digestChallenge.SetFromRequest(httpWebRequest);
788             //
789             // define the part of the challenge we really care about
790             //
791             startingPoint = startingPoint==-1 ? 0 : startingPoint + DigestClient.SignatureSize;
792
793             bool valid;
794             int start, offset, index;
795             string name, value;
796
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;
801             for (;;) {
802                 offset = start;
803                 index = AuthenticationManager.SplitNoQuotes(challenge, ref offset);
804                 if (offset<0) {
805                     break;
806                 }
807                 name = challenge.Substring(start, offset-start);
808                 if (string.Compare(name, DA_charset, StringComparison.OrdinalIgnoreCase)==0) {
809                     if (index<0) {
810                         value = unquote(challenge.Substring(offset+1));
811                     }
812                     else {
813                         value = unquote(challenge.Substring(offset+1, index-offset-1));
814                     }
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;
818                         break;
819                     }
820                 }
821                 if (index<0) {
822                     break;
823                 }
824                 start = ++index;
825             }
826
827             // this time go through the directives, parse them and call defineAttribute()
828             start = startingPoint;
829             for (;;) {
830                 offset = start;
831                 index = AuthenticationManager.SplitNoQuotes(challenge, ref offset);
832                 GlobalLog.Print("HttpDigest::Interpret() SplitNoQuotes() returning index:" + index.ToString() + " offset:" + offset.ToString());
833                 if (offset<0) {
834                     break;
835                 }
836                 name = challenge.Substring(start, offset-start);
837                 if (index<0) {
838                     value = unquote(challenge.Substring(offset+1));
839                 }
840                 else {
841                     value = unquote(challenge.Substring(offset+1, index-offset-1));
842                 }
843                 if (digestChallenge.UTF8Charset) {
844                     bool isAscii = true;
845                     for (int i=0; i<value.Length; i++) {
846                         if (value[i]>(char)0x7F) {
847                             isAscii = false;
848                             break;
849                         }
850                     }
851                     if (!isAscii) {
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];
856                         }
857                         value = Encoding.UTF8.GetString(bytes);
858                         GlobalLog.Print("HttpDigest::Interpret() UTF8 decoded value:[" + value + "]");
859                     }
860                     else {
861                         GlobalLog.Print("HttpDigest::Interpret() no need for special encoding");
862                     }
863                 }
864                 valid = digestChallenge.defineAttribute(name, value);
865                 GlobalLog.Print("HttpDigest::Interpret() defineAttribute(" + name + ", " + value + ") returns " + valid.ToString());
866                 if (index<0 || !valid) {
867                     break;
868                 }
869                 start = ++index;
870             }
871             // We must absolutely have a nonce for Digest to work.
872             if (digestChallenge.Nonce == null) {
873                 if (Logging.On)
874                     Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_requires_nonce));
875                 return null;
876             }
877
878             return digestChallenge;
879         }
880
881         private enum Charset {
882             ASCII,
883             ANSI,
884             UTF8
885         }
886
887         private static string CharsetEncode(string rawString, Charset charset) {
888 #if TRAVE
889             GlobalLog.Print("HttpDigest::CharsetEncode() encoding rawString:[" + rawString + "] Chars(rawString):[" + Chars(rawString) + "] charset:[" + charset + "]");
890 #endif // #if TRAVE
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);
895                 // but it's faster.
896                 char[] chars = new char[bytes.Length];
897                 bytes.CopyTo(chars, 0);
898                 rawString = new string(chars);
899             }
900 #if TRAVE
901             GlobalLog.Print("HttpDigest::CharsetEncode() encoded rawString:[" + rawString + "] Chars(rawString):[" + Chars(rawString) + "] charset:[" + charset + "]");
902 #endif // #if TRAVE
903             return rawString;
904         }
905
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;
916                     break;
917                 }
918             }
919             GlobalLog.Print("HttpDigest::DetectCharset() rawString:[" + rawString + "] has charset:[" + charset.ToString() + "]");
920             return charset;
921         }
922
923 #if TRAVE
924         private static string Chars(string rawString) {
925             string returnString = "[";
926             for (int i=0; i<rawString.Length; i++) {
927                 if (i>0) {
928                     returnString += ",";
929                 }
930                 returnString += ((int)rawString[i]).ToString();
931             }
932             return returnString + "]";
933         }
934 #endif // #if TRAVE
935
936         //
937         // CONSIDER V.NEXT
938         // creating a static hashtable for server nonces and keep track of nonce count
939         //
940         internal static Authorization Authenticate(HttpDigestChallenge digestChallenge, NetworkCredential NC, string spn, ChannelBinding binding) {
941
942             string username = NC.InternalGetUserName();
943             if (ValidationHelper.IsBlankString(username)) {
944                 return null;
945             }
946             string password = NC.InternalGetPassword();
947
948             bool upgraded = IsUpgraded(digestChallenge.Nonce, binding);
949             if (upgraded)
950             {
951                 digestChallenge.ServiceName = spn;
952                 digestChallenge.ChannelBinding = hashChannelBinding(binding, digestChallenge.MD5provider);
953             }
954
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());
958
959                     if (upgraded)
960                     {
961                         digestChallenge.ClientNonce = createUpgradedNonce(digestChallenge);
962                     }
963                     else
964                     {
965                         digestChallenge.ClientNonce = createNonce(32);
966                     }
967
968                     digestChallenge.NonceCount = 1;
969                 }
970                 else {
971                     GlobalLog.Print("HttpDigest::Authenticate() QopPresent:True, reusing nonce. digestChallenge.NonceCount:" + digestChallenge.NonceCount.ToString());
972                     digestChallenge.NonceCount++;
973                 }
974             }
975
976             StringBuilder authorization = new StringBuilder();
977
978             //
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[]
981             //
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.");
985                 return null;
986             }
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.");
990                 return null;
991             }
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(",");
1000                 }
1001                 else {
1002                     authorization.Append(pair(DA_username, CharsetEncode(username, Charset.UTF8), true));
1003                     authorization.Append(",");
1004                     username = CharsetEncode(username, usernameCharset);
1005                 }
1006             }
1007             else {
1008                 // otherwise UTF8 is not required
1009                 username = CharsetEncode(username, usernameCharset);
1010                 authorization.Append(pair(DA_username, username, true));
1011                 authorization.Append(",");
1012             }
1013
1014             password = CharsetEncode(password, passwordCharset);
1015
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));
1022
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
1028                 }
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));
1033                 // RAID#47397
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
1038
1039                 if (upgraded)
1040                 {
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));
1047                 }
1048             }
1049
1050             // warning: this must be computed here
1051             string responseValue = HttpDigest.responseValue(digestChallenge, username, password);
1052             if (responseValue==null) {
1053                 return null;
1054             }
1055
1056             authorization.Append(",");
1057             authorization.Append(pair(DA_response, responseValue, true)); // IE sends quotes - IIS needs them
1058
1059             if (digestChallenge.Opaque!=null) {
1060                 authorization.Append(",");
1061                 authorization.Append(pair(DA_opaque, digestChallenge.Opaque, true));
1062             }
1063
1064             GlobalLog.Print("HttpDigest::Authenticate() digestChallenge.Stale:" + digestChallenge.Stale.ToString());
1065
1066             // completion is decided in Update()
1067             Authorization finalAuthorization = new Authorization(DigestClient.AuthType + " " + authorization.ToString(), false);
1068
1069             return finalAuthorization;
1070         }
1071
1072         private static bool IsUpgraded(string nonce, ChannelBinding binding) {
1073
1074             GlobalLog.Assert(nonce != null, "HttpDigest::IsUpgraded()|'nonce' must not be null.");
1075
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)) {
1080                 return false;
1081             }
1082
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);
1087         }
1088
1089         internal static string unquote(string quotedString) {
1090             return quotedString.Trim().Trim("\"".ToCharArray());
1091         }
1092
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) {
1096             if (quote) {
1097                 return name + "=\"" + value + "\"";
1098             }
1099             return name + "=" + value;
1100         }
1101
1102         //
1103         // this method computes the response-value according to the
1104         // rules described in RFC2831 section 2.1.2.1
1105         //
1106         private static string responseValue(HttpDigestChallenge challenge, string username, string password) {
1107             string secretString = computeSecret(challenge, username, password);
1108             if (secretString == null) {
1109                 return null;
1110             }
1111
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) {
1115                 return null;
1116             }
1117
1118             string secret = hashString(secretString, challenge.MD5provider);
1119             string hexMD2 = hashString(dataString, challenge.MD5provider);
1120
1121             string data =
1122                 challenge.Nonce + ":" +
1123                     (challenge.QopPresent ?
1124                         challenge.NonceCount.ToString("x8", NumberFormatInfo.InvariantInfo) + ":" +
1125                         challenge.ClientNonce + ":" +
1126                         SupportedQuality + ":" + // challenge.QualityOfProtection + ":" +
1127                         hexMD2
1128                         :
1129                         hexMD2);
1130
1131             return hashString(secret + ":" + data, challenge.MD5provider);
1132         }
1133
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;
1137             }
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;
1140             }
1141             if (Logging.On)
1142                 Logging.PrintError(Logging.Web, SR.GetString(SR.net_log_digest_hash_algorithm_not_supported, challenge.Algorithm));
1143             return null;
1144         }
1145
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");
1155
1156         private static int SizeOfInt = Marshal.SizeOf(typeof(int));
1157         private static int MinimumFormattedBindingLength = 5 * SizeOfInt;
1158
1159         //
1160         // Adapted from ComputeGssBindHash() in ds\security\protocols\sspcommon\sspbindings.cxx
1161         //
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
1169         //
1170         private static byte[] formatChannelBindingForHash(ChannelBinding binding)
1171         {
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);
1177
1178             byte[] formattedData = new byte[MinimumFormattedBindingLength + initiatorLength + acceptorLength + applicationDataLength];
1179
1180             BitConverter.GetBytes(initiatorType).CopyTo(formattedData, 0);
1181             BitConverter.GetBytes(initiatorLength).CopyTo(formattedData, SizeOfInt);
1182
1183             int offset = 2 * SizeOfInt;
1184             if (initiatorLength > 0)
1185             {
1186                 int initiatorOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), InitiatorOffsetOffset);
1187                 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), initiatorOffset), formattedData, offset, initiatorLength);
1188                 offset += initiatorLength;
1189             }
1190
1191             BitConverter.GetBytes(acceptorType).CopyTo(formattedData, offset);
1192             BitConverter.GetBytes(acceptorLength).CopyTo(formattedData, offset + SizeOfInt);
1193
1194             offset += 2 * SizeOfInt;
1195             if (acceptorLength > 0)
1196             {
1197                 int acceptorOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), AcceptorOffsetOffset);
1198                 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), acceptorOffset), formattedData, offset, acceptorLength);
1199                 offset += acceptorLength;
1200             }
1201
1202             BitConverter.GetBytes(applicationDataLength).CopyTo(formattedData, offset);
1203
1204             offset += SizeOfInt;
1205             if (applicationDataLength > 0)
1206             {
1207                 int applicationDataOffset = Marshal.ReadInt32(binding.DangerousGetHandle(), ApplicationDataOffsetOffset);
1208                 Marshal.Copy(IntPtrHelper.Add(binding.DangerousGetHandle(), applicationDataOffset), formattedData, offset, applicationDataLength);
1209             }
1210
1211             return formattedData;
1212         }
1213
1214         private static string hashChannelBinding(ChannelBinding binding, MD5CryptoServiceProvider MD5provider)
1215         {
1216             if (binding == null)
1217             {
1218                 return ZeroChannelBindingHash;
1219             }
1220
1221             byte[] formattedData = formatChannelBindingForHash(binding);
1222             byte[] hash = MD5provider.ComputeHash(formattedData);
1223
1224             return hexEncode(hash);
1225         }
1226
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];
1232             }
1233             byte[] hash = MD5provider.ComputeHash(encodedBytes);
1234             string hashString = hexEncode(hash);
1235             GlobalLog.Leave("HttpDigest::hashString", "[" + hashString.Length.ToString() + ":" + hashString + "]");
1236             return hashString;
1237         }
1238
1239         private static string hexEncode(byte[] rawbytes) {
1240             int size = rawbytes.Length;
1241             char[] wa = new char[2*size];
1242
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];
1247             }
1248
1249             return new string(wa);
1250         }
1251
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];
1262             }
1263             return new string(digits);
1264         }
1265
1266         private static string createUpgradedNonce(HttpDigestChallenge digestChallenge)
1267         {
1268             string hashMe = digestChallenge.ServiceName + ":" + digestChallenge.ChannelBinding;
1269             byte[] hash = digestChallenge.MD5provider.ComputeHash(Encoding.ASCII.GetBytes(hashMe));
1270
1271             return UpgradedV1 + hexEncode(hash) + createNonce(32);
1272         }
1273     }
1274
1275 }