Bug 378787. Digest Authentication Request Splitting. r=biesi, sr=brendan
authorsayrer@gmail.com
Thu, 03 May 2007 20:31:30 -0700
changeset 1073 d1728ef839b8fa88b5dccc1e6ea7e61d829915bf
parent 1072 793764886e785af2e8ff10745812b80d9b0f89b2
child 1074 cf54509d6516661a76c99de7c95d0983b040a56b
push id1
push userbsmedberg@mozilla.com
push dateThu, 20 Mar 2008 16:49:24 +0000
treeherdermozilla-central@61007906a1f8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbiesi, brendan
bugs378787
milestone1.9a5pre
Bug 378787. Digest Authentication Request Splitting. r=biesi, sr=brendan
netwerk/protocol/http/src/nsHttpDigestAuth.cpp
netwerk/protocol/http/src/nsHttpDigestAuth.h
netwerk/test/unit/test_authentication.js
--- a/netwerk/protocol/http/src/nsHttpDigestAuth.cpp
+++ b/netwerk/protocol/http/src/nsHttpDigestAuth.cpp
@@ -333,57 +333,78 @@ nsHttpDigestAuth::GenerateCredentials(ns
 
   rv = CalculateHA2(httpMethod, path, qop, upload_data_digest, ha2_digest);
   if (NS_FAILED(rv)) return rv;
 
   rv = CalculateResponse(ha1_digest, ha2_digest, nonce, qop, nonce_count,
                          cnonce, response_digest);
   if (NS_FAILED(rv)) return rv;
 
+  //
+  // Values that need to match the quoted-string production from RFC 2616:
+  //
+  //    username
+  //    realm
+  //    nonce
+  //    opaque
+  //    cnonce
+  //
+
   nsCAutoString authString;
-  authString.AssignLiteral("Digest username=\"");
-  authString += cUser;
-  authString.AppendLiteral("\", realm=\"");
-  authString += realm;
-  authString.AppendLiteral("\", nonce=\"");
-  authString += nonce;
-  authString.AppendLiteral("\", uri=\"");
+
+  authString.AssignLiteral("Digest username=");
+  rv = AppendQuotedString(cUser, authString);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  authString.AppendLiteral(", realm=");
+  rv = AppendQuotedString(realm, authString);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  authString.AppendLiteral(", nonce=");
+  rv = AppendQuotedString(nonce, authString);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  authString.AppendLiteral(", uri=\"");
   authString += path;
   if (algorithm & ALGO_SPECIFIED) {
     authString.AppendLiteral("\", algorithm=");
     if (algorithm & ALGO_MD5_SESS)
       authString.AppendLiteral("MD5-sess");
     else
       authString.AppendLiteral("MD5");
   } else {
     authString += '\"';
   }
   authString.AppendLiteral(", response=\"");
   authString += response_digest;
+  authString += '\"';
 
   if (!opaque.IsEmpty()) {
-    authString.AppendLiteral("\", opaque=\"");
-    authString += opaque;
+    authString.AppendLiteral(", opaque=");
+    rv = AppendQuotedString(opaque, authString);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (qop) {
-    authString.AppendLiteral("\", qop=");
+    authString.AppendLiteral(", qop=");
     if (requireExtraQuotes)
       authString += '\"';
     authString.AppendLiteral("auth");
     if (qop & QOP_AUTH_INT)
       authString.AppendLiteral("-int");
     if (requireExtraQuotes)
       authString += '\"';
     authString.AppendLiteral(", nc=");
     authString += nonce_count;
-    authString.AppendLiteral(", cnonce=\"");
-    authString += cnonce;
+
+    authString.AppendLiteral(", cnonce=");
+    rv = AppendQuotedString(cnonce, authString);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
-  authString += '\"';
+
 
   *creds = ToNewCString(authString);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsHttpDigestAuth::GetAuthFlags(PRUint32 *flags)
 {
@@ -664,9 +685,44 @@ nsHttpDigestAuth::ParseChallenge(const c
             nsCRT::strncasecmp(challenge+algostart, "auth-int", 8) == 0)
           *qop |= QOP_AUTH_INT;
       }
     }
   }
   return NS_OK;
 }
 
+nsresult
+nsHttpDigestAuth::AppendQuotedString(const nsACString & value,
+                                     nsACString & aHeaderLine)
+{
+  nsCAutoString quoted;
+  nsACString::const_iterator s, e;
+  value.BeginReading(s);
+  value.EndReading(e);
+
+  //
+  // Encode string according to RFC 2616 quoted-string production
+  //
+  quoted.Append('"');
+  for ( ; s != e; ++s) {
+    //
+    // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
+    //
+    if (*s <= 31 || *s == 127) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // Escape two syntactically significant characters
+    if (*s == '"' || *s == '\\') {
+      quoted.Append('\\');
+    }
+
+    quoted.Append(*s);
+  }
+  // FIXME: bug 41489
+  // We should RFC2047-encode non-Latin-1 values according to spec
+  quoted.Append('"');
+  aHeaderLine.Append(quoted);
+  return NS_OK;
+}
+
 // vim: ts=2 sw=2
--- a/netwerk/protocol/http/src/nsHttpDigestAuth.h
+++ b/netwerk/protocol/http/src/nsHttpDigestAuth.h
@@ -104,14 +104,18 @@ class nsHttpDigestAuth : public nsIHttpA
                             PRUint16 * algorithm,
                             PRUint16 * qop);
 
     // result is in mHashBuf
     nsresult MD5Hash(const char *buf, PRUint32 len);
 
     nsresult GetMethodAndPath(nsIHttpChannel *, PRBool, nsCString &, nsCString &);
 
+    // append the quoted version of value to aHeaderLine
+    nsresult AppendQuotedString(const nsACString & value,
+                                nsACString & aHeaderLine);
+
   protected:
     nsCOMPtr<nsICryptoHash>        mVerifier;
     char                           mHashBuf[DIGEST_LENGTH];
 };
 
 #endif // nsHttpDigestAuth_h__
--- a/netwerk/test/unit/test_authentication.js
+++ b/netwerk/test/unit/test_authentication.js
@@ -80,35 +80,36 @@ AuthPrompt2.prototype = {
       return this;
     throw Components.results.NS_ERROR_NO_INTERFACE;
   },
 
   promptAuth:
     function ap2_promptAuth(channel, level, authInfo)
   {
     var isNTLM = channel.URI.path.indexOf("ntlm") != -1;
+    var isDigest = channel.URI.path.indexOf("digest") != -1;
 
     if (isNTLM)
       this.expectedRealm = ""; // NTLM knows no realms
 
     do_check_eq(this.expectedRealm, authInfo.realm);
 
-    var expectedLevel = isNTLM ?
+    var expectedLevel = (isNTLM || isDigest) ?
                         nsIAuthPrompt2.LEVEL_PW_ENCRYPTED :
                         nsIAuthPrompt2.LEVEL_NONE;
     do_check_eq(expectedLevel, level);
 
     var expectedFlags = nsIAuthInformation.AUTH_HOST;
 
     if (isNTLM)
       expectedFlags |= nsIAuthInformation.NEED_DOMAIN;
 
     do_check_eq(expectedFlags, authInfo.flags);
 
-    var expectedScheme = isNTLM ? "ntlm" : "basic";
+    var expectedScheme = isNTLM ? "ntlm" : isDigest ? "digest" : "basic";
     do_check_eq(expectedScheme, authInfo.authenticationScheme);
 
     // No passwords in the URL -> nothing should be prefilled
     do_check_eq(authInfo.username, "");
     do_check_eq(authInfo.password, "");
     do_check_eq(authInfo.domain, "");
 
     if (this.flags & FLAG_RETURN_FALSE)
@@ -125,19 +126,20 @@ AuthPrompt2.prototype = {
     return true;
   },
 
   asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) {
     do_throw("not implemented yet")
   }
 };
 
-function Requestor(flags, versions) {
+function Requestor(flags, versions, username) {
   this.flags = flags;
   this.versions = versions;
+  this.username = username;
 }
 
 Requestor.prototype = {
   QueryInterface: function requestor_qi(iid) {
     if (iid.equals(Components.interfaces.nsISupports) ||
         iid.equals(Components.interfaces.nsIInterfaceRequestor))
       return this;
     throw Components.results.NS_ERROR_NO_INTERFACE;
@@ -150,17 +152,19 @@ Requestor.prototype = {
       if (!this.prompt1)
         this.prompt1 = new AuthPrompt1(this.flags); 
       return this.prompt1;
     }
     if (this.versions & 2 &&
         iid.equals(Components.interfaces.nsIAuthPrompt2)) {
       // Allow the prompt to store state by caching it here
       if (!this.prompt2)
-        this.prompt2 = new AuthPrompt2(this.flags); 
+        this.prompt2 = new AuthPrompt2(this.flags);
+      if (this.username)
+        this.prompt2.user = this.username;
       return this.prompt2;
     }
 
     throw Components.results.NS_ERROR_NO_INTERFACE;
   },
 
   prompt1: null,
   prompt2: null
@@ -246,27 +250,29 @@ function makeChan(url) {
   var chan = ios.newChannel(url, null, null)
                 .QueryInterface(Components.interfaces.nsIHttpChannel);
 
   return chan;
 }
 
 var tests = [test_noauth, test_returnfalse1, test_wrongpw1, test_prompt1,
              test_returnfalse2, test_wrongpw2, test_prompt2, test_ntlm,
-             test_auth];
+             test_auth, test_digest_noauth, test_digest,
+             test_digest_bogus_user];
 var current_test = 0;
 
 var httpserv = null;
 
 function run_test() {
   httpserv = new nsHttpServer();
 
   httpserv.registerPathHandler("/auth", authHandler);
   httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple);
   httpserv.registerPathHandler("/auth/realm", authRealm);
+  httpserv.registerPathHandler("/auth/digest", authDigest);
 
   httpserv.start(4444);
 
   tests[0]();
 }
 
 function test_noauth() {
   var chan = makeChan("http://localhost:4444/auth");
@@ -352,16 +358,45 @@ function test_auth() {
 
   chan.notificationCallbacks = new RealmTestRequestor();
   listener.expectedCode = 401; // Unauthorized
   chan.asyncOpen(listener, null);
 
   do_test_pending();
 }
 
+function test_digest_noauth() {
+  var chan = makeChan("http://localhost:4444/auth/digest");
+
+  //chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
+  listener.expectedCode = 401; // Unauthorized
+  chan.asyncOpen(listener, null);
+
+  do_test_pending();
+}
+
+function test_digest() {
+  var chan = makeChan("http://localhost:4444/auth/digest");
+
+  chan.notificationCallbacks = new Requestor(0, 2);
+  listener.expectedCode = 200; // OK
+  chan.asyncOpen(listener, null);
+
+  do_test_pending();
+}
+
+function test_digest_bogus_user() {
+  var chan = makeChan("http://localhost:4444/auth/digest");
+  chan.notificationCallbacks =  new Requestor(0, 2, "foo\nbar");
+  listener.expectedCode = 401; // unauthorized
+  chan.asyncOpen(listener, null);
+
+  do_test_pending();
+}
+
 // PATH HANDLERS
 
 // /auth
 function authHandler(metadata, response) {
   // btoa("guest:guest"), but that function is not available here
   var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
 
   var body;
@@ -401,8 +436,87 @@ function authNtlmSimple(metadata, respon
 // /auth/realm
 function authRealm(metadata, response) {
   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
   response.setHeader("WWW-Authenticate", 'Basic realm="\\"foo_bar"', false);
   var body = "success";
 
   response.bodyOutputStream.write(body, body.length);
 }
+
+//
+// Digest functions
+// 
+function bytesFromString(str) {
+ var converter =
+   Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+     .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var result = {};
+ var data = converter.convertToByteArray(str, result);
+ return data;
+}
+
+// return the two-digit hexadecimal code for a byte
+function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+}
+
+function H(str) {
+ var data = bytesFromString(str);
+ var ch = Components.classes["@mozilla.org/security/hash;1"]
+            .createInstance(Components.interfaces.nsICryptoHash);
+ ch.init(Components.interfaces.nsICryptoHash.MD5);
+ ch.update(data, data.length);
+ var hash = ch.finish(false);
+ return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+//
+// Digest handler
+//
+// /auth/digest
+function authDigest(metadata, response) {
+ var nonce = "6f93719059cf8d568005727f3250e798";
+ var opaque = "1234opaque1234";
+ var cnonceRE = /cnonce="(\w+)"/;
+ var responseRE = /response="(\w+)"/;
+ var usernameRE = /username="(\w+)"/;
+ var authenticate = 'Digest realm="secret", domain="/",  qop=auth,' +
+                    'algorithm=MD5, nonce="' + nonce+ '" opaque="' + 
+                     opaque + '"';
+ var body;
+ // check creds if we have them
+ if (metadata.hasHeader("Authorization")) {
+   var auth = metadata.getHeader("Authorization");
+   var cnonce = (auth.match(cnonceRE))[1];
+   var clientDigest = (auth.match(responseRE))[1];
+   var username = (auth.match(usernameRE))[1];
+   var nc = "00000001";
+   
+   if (username != "guest") {
+     response.setStatusLine(metadata.httpVersion, 400, "bad request");
+     body = "should never get here";
+   } else {
+     // see RFC2617 for the description of this calculation
+     var A1 = "guest:secret:guest";
+     var A2 = "GET:/auth/digest";
+     var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
+     var digest = H([H(A1), noncebits].join(":"));
+
+     if (clientDigest == digest) {
+       response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+       body = "success";
+     } else {
+       response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+       response.setHeader("WWW-Authenticate", authenticate, false);
+       body = "auth failed";
+     }
+   }
+ } else {
+   // no header, send one
+   response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+   response.setHeader("WWW-Authenticate", authenticate, false);
+   body = "failed, no header";
+ }
+ 
+ response.bodyOutputStream.write(body, body.length);
+}