Browse Source

2003-12-02 Gonzalo Paniagua Javier <[email protected]>

	* DigestClient.cs: merged in code from Sebastien Pouliot and Greg
	Reinacker that Supports cnonce and preauthentication.

svn path=/trunk/mcs/; revision=20703
Gonzalo Paniagua Javier 22 years ago
parent
commit
8397bdc061
2 changed files with 216 additions and 53 deletions
  1. 5 0
      mcs/class/System/System.Net/ChangeLog
  2. 211 53
      mcs/class/System/System.Net/DigestClient.cs

+ 5 - 0
mcs/class/System/System.Net/ChangeLog

@@ -1,3 +1,8 @@
+2003-12-02  Gonzalo Paniagua Javier <[email protected]>
+
+	* DigestClient.cs: merged in code from Sebastien Pouliot and Greg
+	Reinacker that Supports cnonce and preauthentication.
+
 2003-12-02  Gonzalo Paniagua Javier <[email protected]>
 
 	* DigestClient.cs: initial Digest authentication. Works with apache

+ 211 - 53
mcs/class/System/System.Net/DigestClient.cs

@@ -2,32 +2,45 @@
 // System.Net.DigestClient.cs
 //
 // Authors:
-//	Gonzalo Paniagua Javier ([email protected])
+//	Greg Reinacker ([email protected])
+//	Sebastien Pouliot ([email protected])
+//	Gonzalo Paniagua Javier ([email protected]
 //
-// (C) 2003 Novell, Inc (http://www.ximian.com)
+// Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
+// Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
+// (c) 2003 Novell, Inc. (http://www.novell.com)
+//
+// Original (server-side) source code available at
+// http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
 //
 
+using System;
+using System.Collections;
+using System.Collections.Specialized;
 using System.IO;
+using System.Net;
 using System.Security.Cryptography;
 using System.Text;
+
 namespace System.Net
 {
 	//
 	// This works with apache mod_digest
 	//TODO:
 	//	MD5-sess
-	//	qop
-	//	cnonce et al.
+	//	qop (auth-int)
+	//
 	//	See RFC 2617 for details.
 	//
 
+
 	class DigestHeaderParser
 	{
 		string header;
 		int length;
 		int pos;
 		string realm, opaque, nonce, algorithm;
-		static string [] keywords = { "realm", "opaque", "nonce", "algorithm" };
+		static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
 		string [] values = new string [keywords.Length];
 
 		public DigestHeaderParser (string header)
@@ -51,6 +64,10 @@ namespace System.Net
 			get { return values [3]; }
 		}
 		
+		public string QOP {
+			get { return values [4]; }
+		}
+		
 		public bool Parse ()
 		{
 			if (!header.ToLower ().StartsWith ("digest "))
@@ -139,84 +156,225 @@ namespace System.Net
 			return true;
 		}
 	}
-	
-	class DigestClient : IAuthenticationModule
+
+	class DigestSession
 	{
-		public DigestClient ()
+		static RandomNumberGenerator rng;
+		
+		static DigestSession () 
 		{
+			rng = RandomNumberGenerator.Create ();
+		}
+
+		private int _nc;
+		private HashAlgorithm hash;
+		private DigestHeaderParser parser;
+		private string _cnonce;
+
+		public DigestSession () 
+		{
+			_nc = 1;
+		}
+
+		public string Algorithm {
+			get { return parser.Algorithm; }
+		}
+
+		public string Realm {
+			get { return parser.Realm; }
+		}
+
+		public string Nonce {
+			get { return parser.Nonce; }
 		}
 
-		static string GetHexString (byte [] bytes)
+		public string Opaque {
+			get { return parser.Opaque; }
+		}
+
+		public string QOP {
+			get { return parser.QOP; }
+		}
+
+		public string CNonce {
+			get { 
+				if (_cnonce == null) {
+					// 15 is a multiple of 3 which is better for base64 because it
+					// wont end with '=' and risk messing up the server parsing
+					byte[] bincnonce = new byte [15];
+					rng.GetBytes (bincnonce);
+					_cnonce = Convert.ToBase64String (bincnonce);
+					Array.Clear (bincnonce, 0, bincnonce.Length);
+				}
+				return _cnonce;
+			}
+		}
+
+		public bool Parse (string challenge) 
 		{
-			StringBuilder result = new StringBuilder (bytes.Length * 2);
-			foreach (byte b in bytes)
-				result.AppendFormat ("{0:x2}", (int) b);
+			parser = new DigestHeaderParser (challenge);
+			if (!parser.Parse ()) {
+				Console.WriteLine ("Parser");
+				return false;
+			}
+
+			// build the hash object (only MD5 is defined in RFC2617)
+			if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
+				hash = HashAlgorithm.Create ("MD5");
 
-			return result.ToString ();
+			return true;
 		}
 
-		public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials)
+		private string HashToHexString (string toBeHashed) 
 		{
-			if (credentials == null || challenge == null)
+			if (hash == null)
 				return null;
 
+			hash.Initialize ();
+			byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
+
+			StringBuilder sb = new StringBuilder ();
+			foreach (byte b in result)
+				sb.Append (b.ToString ("x2"));
+			return sb.ToString ();
+		}
+
+		private string HA1 (string username, string password) 
+		{
+			string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
+			if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
+				ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
+			return HashToHexString (ha1);
+		}
+
+		private string HA2 (HttpWebRequest webRequest) 
+		{
+			string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
+			if (QOP == "auth-int") {
+				// TODO
+				// ha2 += String.Format (":{0}", hentity);
+			}		
+			return HashToHexString (ha2);
+		}
+
+		private string Response (string username, string password, HttpWebRequest webRequest) 
+		{
+			string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
+			if (QOP != null)
+				response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
+			response += HA2 (webRequest);
+			return HashToHexString (response);
+		}
+
+		public Authorization Authenticate (WebRequest webRequest, ICredentials credentials) 
+		{
+			if (parser == null)
+				throw new InvalidOperationException ();
+
 			HttpWebRequest request = webRequest as HttpWebRequest;
 			if (request == null)
 				return null;
-
-			NetworkCredential cred = credentials.GetCredential (request.AuthUri, "digest");
+	
+			NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
 			string userName = cred.UserName;
 			if (userName == null || userName == "")
 				return null;
 
-			DigestHeaderParser parser = new DigestHeaderParser (challenge);
-			if (!parser.Parse ())
+			string password = cred.Password;
+	
+			StringBuilder auth = new StringBuilder ();
+			auth.AppendFormat ("Digest username=\"{0}\", ", userName);
+			auth.AppendFormat ("realm=\"{0}\", ", Realm);
+			auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
+			auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
+
+			if (QOP != null) // quality of protection (server decision)
+				auth.AppendFormat ("qop=\"{0}\", ", QOP);
+
+			if (Algorithm != null) // hash algorithm (only MD5 in RFC2617)
+				auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
+
+			lock (this) {
+				// _nc MUST NOT change from here...
+				// number of request using this nonce
+				if (QOP != null) {
+					auth.AppendFormat ("nc={0:X8}, ", _nc);
+					_nc++;
+				}
+				// until here, now _nc can change
+			}
+
+			if (QOP != null) // opaque value from the client
+				auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
+
+			if (Opaque != null) // exact same opaque value as received from server
+				auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
+
+			auth.AppendFormat ("response=\"{0}\"", Response (userName, password, request));
+			return new Authorization (auth.ToString ());
+		}
+	}
+
+	class DigestClient : IAuthenticationModule
+	{
+
+		static Hashtable cache;		// cache entries by nonce
+
+		static DigestClient () 
+		{
+			cache = Hashtable.Synchronized (new Hashtable ());
+		}
+	
+		public DigestClient () {}
+	
+		// IAuthenticationModule
+	
+		public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials) 
+		{
+			if (credentials == null || challenge == null)
+				return null;
+	
+			string header = challenge.Trim ();
+			if (!header.ToLower ().StartsWith ("digest "))
 				return null;
 
-			Encoding enc = Encoding.Default;
-			MD5 md5 = new MD5CryptoServiceProvider ();
-			string password = cred.Password;
+			HttpWebRequest request = webRequest as HttpWebRequest;
+			if (request == null)
+				return null;
 
-			// A1: user ":" realm ":" password
-			string a1str = String.Format ("{0}:{1}:{2}", userName, parser.Realm, password);
-			byte [] a1 = md5.ComputeHash (enc.GetBytes (a1str));
-			a1str = GetHexString (a1);
-			// A2: method ":" path
-			string a2str = String.Format ("{0}:{1}", request.Method, request.Address.PathAndQuery);
-			byte [] a2 = md5.ComputeHash (enc.GetBytes (a2str));
-			a2str = GetHexString (a2);
-			
-			// Response: a1 ":" nonce ":" a2
-			string respString = String.Format ("{0}:{1}:{2}", a1str, parser.Nonce, a2str);
-			byte [] respBytes = md5.ComputeHash (enc.GetBytes (respString));
-			respString = GetHexString (respBytes);
+			DigestSession ds = (DigestSession) cache [request.Address];
+			bool addDS = (ds == null);
+			if (addDS)
+				ds = new DigestSession ();
 
-			StringBuilder response = new StringBuilder ();
-			response.AppendFormat ("Digest username=\"{0}\", ", userName);
-			response.AppendFormat ("realm=\"{0}\", ", parser.Realm);
-			response.AppendFormat ("nonce=\"{0}\", ", parser.Nonce);
-			response.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
-			response.AppendFormat ("response=\"{0}\"", respString);
-			if (parser.Opaque != null)
-				response.AppendFormat (", opaque=\"{0}\"", parser.Opaque);
+			if (!ds.Parse (challenge))
+				return null;
 
-			if (parser.Algorithm != null)
-				response.AppendFormat (", algorithm=\"{0}\"", parser.Algorithm);
+			if (addDS)
+				cache.Add (request.Address, ds);
 
-			return new Authorization (response.ToString ());
+			return ds.Authenticate (webRequest, credentials);
 		}
 
-		[MonoTODO]
-		public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials)
+		public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials) 
 		{
-			throw new NotImplementedException ();
-		}
+			HttpWebRequest request = webRequest as HttpWebRequest;
+			if (request == null)
+				return null;
 
-		public string AuthenticationType {
+			// check cache for URI
+			DigestSession ds = (DigestSession) cache [request.Address];
+			if (ds == null)
+				return null;
+
+			return ds.Authenticate (webRequest, credentials);
+		}
+	
+		public string AuthenticationType { 
 			get { return "Digest"; }
 		}
-
-		public bool CanPreAuthenticate {
+	
+		public bool CanPreAuthenticate { 
 			get { return true; }
 		}
 	}