Home | History | Annotate | Download | only in http
      1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "net/http/http_auth_handler_digest.h"
      6 
      7 #include "base/md5.h"
      8 #include "base/rand_util.h"
      9 #include "base/string_util.h"
     10 #include "net/base/net_util.h"
     11 #include "net/http/http_auth.h"
     12 #include "net/http/http_request_info.h"
     13 #include "net/http/http_util.h"
     14 
     15 // TODO(eroman): support qop=auth-int
     16 
     17 namespace net {
     18 
     19 // Digest authentication is specified in RFC 2617.
     20 // The expanded derivations are listed in the tables below.
     21 
     22 //==========+==========+==========================================+
     23 //    qop   |algorithm |               response                   |
     24 //==========+==========+==========================================+
     25 //    ?     |  ?, md5, | MD5(MD5(A1):nonce:MD5(A2))               |
     26 //          | md5-sess |                                          |
     27 //--------- +----------+------------------------------------------+
     28 //   auth,  |  ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) |
     29 // auth-int | md5-sess |                                          |
     30 //==========+==========+==========================================+
     31 //    qop   |algorithm |                  A1                      |
     32 //==========+==========+==========================================+
     33 //          | ?, md5   | user:realm:password                      |
     34 //----------+----------+------------------------------------------+
     35 //          | md5-sess | MD5(user:realm:password):nonce:cnonce    |
     36 //==========+==========+==========================================+
     37 //    qop   |algorithm |                  A2                      |
     38 //==========+==========+==========================================+
     39 //  ?, auth |          | req-method:req-uri                       |
     40 //----------+----------+------------------------------------------+
     41 // auth-int |          | req-method:req-uri:MD5(req-entity-body)  |
     42 //=====================+==========================================+
     43 
     44 
     45 // static
     46 std::string HttpAuthHandlerDigest::GenerateNonce() {
     47   // This is how mozilla generates their cnonce -- a 16 digit hex string.
     48   static const char domain[] = "0123456789abcdef";
     49   std::string cnonce;
     50   cnonce.reserve(16);
     51   for (int i = 0; i < 16; ++i)
     52     cnonce.push_back(domain[base::RandInt(0, 15)]);
     53   return cnonce;
     54 }
     55 
     56 // static
     57 std::string HttpAuthHandlerDigest::QopToString(int qop) {
     58   switch (qop) {
     59     case QOP_AUTH:
     60       return "auth";
     61     case QOP_AUTH_INT:
     62       return "auth-int";
     63     default:
     64       return "";
     65   }
     66 }
     67 
     68 // static
     69 std::string HttpAuthHandlerDigest::AlgorithmToString(int algorithm) {
     70   switch (algorithm) {
     71     case ALGORITHM_MD5:
     72       return "MD5";
     73     case ALGORITHM_MD5_SESS:
     74       return "MD5-sess";
     75     default:
     76       return "";
     77   }
     78 }
     79 
     80 std::string HttpAuthHandlerDigest::GenerateCredentials(
     81     const std::wstring& username,
     82     const std::wstring& password,
     83     const HttpRequestInfo* request,
     84     const ProxyInfo* proxy) {
     85   // Generate a random client nonce.
     86   std::string cnonce = GenerateNonce();
     87 
     88   // The nonce-count should be incremented after re-use per the spec.
     89   // This may not be possible when there are multiple connections to the
     90   // server though:
     91   // https://bugzilla.mozilla.org/show_bug.cgi?id=114451
     92   int nonce_count = ++nonce_count_;
     93 
     94   // Extract the request method and path -- the meaning of 'path' is overloaded
     95   // in certain cases, to be a hostname.
     96   std::string method;
     97   std::string path;
     98   GetRequestMethodAndPath(request, proxy, &method, &path);
     99 
    100   return AssembleCredentials(method, path,
    101                              // TODO(eroman): is this the right encoding?
    102                              WideToUTF8(username),
    103                              WideToUTF8(password),
    104                              cnonce, nonce_count);
    105 }
    106 
    107 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
    108     const HttpRequestInfo* request,
    109     const ProxyInfo* proxy,
    110     std::string* method,
    111     std::string* path) const {
    112   DCHECK(request);
    113   DCHECK(proxy);
    114 
    115   const GURL& url = request->url;
    116 
    117   if (target_ == HttpAuth::AUTH_PROXY && url.SchemeIs("https")) {
    118     *method = "CONNECT";
    119     *path = GetHostAndPort(url);
    120   } else {
    121     *method = request->method;
    122     *path = HttpUtil::PathForRequest(url);
    123   }
    124 }
    125 
    126 std::string HttpAuthHandlerDigest::AssembleResponseDigest(
    127     const std::string& method,
    128     const std::string& path,
    129     const std::string& username,
    130     const std::string& password,
    131     const std::string& cnonce,
    132     const std::string& nc) const {
    133   // ha1 = MD5(A1)
    134   std::string ha1 = MD5String(username + ":" + realm_ + ":" + password);
    135   if (algorithm_ == HttpAuthHandlerDigest::ALGORITHM_MD5_SESS)
    136     ha1 = MD5String(ha1 + ":" + nonce_ + ":" + cnonce);
    137 
    138   // ha2 = MD5(A2)
    139   // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int.
    140   std::string ha2 = MD5String(method + ":" + path);
    141 
    142   std::string nc_part;
    143   if (qop_ != HttpAuthHandlerDigest::QOP_UNSPECIFIED) {
    144     nc_part = nc + ":" + cnonce + ":" + QopToString(qop_) + ":";
    145   }
    146 
    147   return MD5String(ha1 + ":" + nonce_ + ":" + nc_part + ha2);
    148 }
    149 
    150 std::string HttpAuthHandlerDigest::AssembleCredentials(
    151     const std::string& method,
    152     const std::string& path,
    153     const std::string& username,
    154     const std::string& password,
    155     const std::string& cnonce,
    156     int nonce_count) const {
    157   // the nonce-count is an 8 digit hex string.
    158   std::string nc = StringPrintf("%08x", nonce_count);
    159 
    160   std::string authorization = std::string("Digest username=") +
    161       HttpUtil::Quote(username);
    162   authorization += ", realm=" + HttpUtil::Quote(realm_);
    163   authorization += ", nonce=" + HttpUtil::Quote(nonce_);
    164   authorization += ", uri=" + HttpUtil::Quote(path);
    165 
    166   if (algorithm_ != ALGORITHM_UNSPECIFIED) {
    167     authorization += ", algorithm=" + AlgorithmToString(algorithm_);
    168   }
    169   std::string response = AssembleResponseDigest(method, path, username,
    170                                                 password, cnonce, nc);
    171   // No need to call HttpUtil::Quote() as the response digest cannot contain
    172   // any characters needing to be escaped.
    173   authorization += ", response=\"" + response + "\"";
    174 
    175   if (!opaque_.empty()) {
    176     authorization += ", opaque=" + HttpUtil::Quote(opaque_);
    177   }
    178   if (qop_ != QOP_UNSPECIFIED) {
    179     // TODO(eroman): Supposedly IIS server requires quotes surrounding qop.
    180     authorization += ", qop=" + QopToString(qop_);
    181     authorization += ", nc=" + nc;
    182     authorization += ", cnonce=" + HttpUtil::Quote(cnonce);
    183   }
    184 
    185   return authorization;
    186 }
    187 
    188 // The digest challenge header looks like:
    189 //   WWW-Authenticate: Digest
    190 //     [realm="<realm-value>"]
    191 //     nonce="<nonce-value>"
    192 //     [domain="<list-of-URIs>"]
    193 //     [opaque="<opaque-token-value>"]
    194 //     [stale="<true-or-false>"]
    195 //     [algorithm="<digest-algorithm>"]
    196 //     [qop="<list-of-qop-values>"]
    197 //     [<extension-directive>]
    198 //
    199 // Note that according to RFC 2617 (section 1.2) the realm is required.
    200 // However we allow it to be omitted, in which case it will default to the
    201 // empty string.
    202 //
    203 // This allowance is for better compatibility with webservers that fail to
    204 // send the realm (See http://crbug.com/20984 for an instance where a
    205 // webserver was not sending the realm with a BASIC challenge).
    206 bool HttpAuthHandlerDigest::ParseChallenge(
    207     std::string::const_iterator challenge_begin,
    208     std::string::const_iterator challenge_end) {
    209   scheme_ = "digest";
    210   score_ = 2;
    211   properties_ = ENCRYPTS_IDENTITY;
    212 
    213   // Initialize to defaults.
    214   stale_ = false;
    215   algorithm_ = ALGORITHM_UNSPECIFIED;
    216   qop_ = QOP_UNSPECIFIED;
    217   realm_ = nonce_ = domain_ = opaque_ = std::string();
    218 
    219   HttpAuth::ChallengeTokenizer props(challenge_begin, challenge_end);
    220 
    221   if (!props.valid() || !LowerCaseEqualsASCII(props.scheme(), "digest"))
    222     return false; // FAIL -- Couldn't match auth-scheme.
    223 
    224   // Loop through all the properties.
    225   while (props.GetNext()) {
    226     if (props.value().empty()) {
    227       DLOG(INFO) << "Invalid digest property";
    228       return false;
    229     }
    230 
    231     if (!ParseChallengeProperty(props.name(), props.unquoted_value()))
    232       return false; // FAIL -- couldn't parse a property.
    233   }
    234 
    235   // Check if tokenizer failed.
    236   if (!props.valid())
    237     return false; // FAIL
    238 
    239   // Check that a minimum set of properties were provided.
    240   if (nonce_.empty())
    241     return false; // FAIL
    242 
    243   return true;
    244 }
    245 
    246 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string& name,
    247                                                    const std::string& value) {
    248   if (LowerCaseEqualsASCII(name, "realm")) {
    249     realm_ = value;
    250   } else if (LowerCaseEqualsASCII(name, "nonce")) {
    251     nonce_ = value;
    252   } else if (LowerCaseEqualsASCII(name, "domain")) {
    253     domain_ = value;
    254   } else if (LowerCaseEqualsASCII(name, "opaque")) {
    255     opaque_ = value;
    256   } else if (LowerCaseEqualsASCII(name, "stale")) {
    257     // Parse the stale boolean.
    258     stale_ = LowerCaseEqualsASCII(value, "true");
    259   } else if (LowerCaseEqualsASCII(name, "algorithm")) {
    260     // Parse the algorithm.
    261     if (LowerCaseEqualsASCII(value, "md5")) {
    262       algorithm_ = ALGORITHM_MD5;
    263     } else if (LowerCaseEqualsASCII(value, "md5-sess")) {
    264       algorithm_ = ALGORITHM_MD5_SESS;
    265     } else {
    266       DLOG(INFO) << "Unknown value of algorithm";
    267       return false; // FAIL -- unsupported value of algorithm.
    268     }
    269   } else if (LowerCaseEqualsASCII(name, "qop")) {
    270     // Parse the comma separated list of qops.
    271     HttpUtil::ValuesIterator qop_values(value.begin(), value.end(), ',');
    272     while (qop_values.GetNext()) {
    273       if (LowerCaseEqualsASCII(qop_values.value(), "auth")) {
    274         qop_ |= QOP_AUTH;
    275       } else if (LowerCaseEqualsASCII(qop_values.value(), "auth-int")) {
    276         qop_ |= QOP_AUTH_INT;
    277       }
    278     }
    279   } else {
    280     DLOG(INFO) << "Skipping unrecognized digest property";
    281     // TODO(eroman): perhaps we should fail instead of silently skipping?
    282   }
    283   return true;
    284 }
    285 
    286 }  // namespace net
    287