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