1 // Copyright (c) 2011 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 <string> 8 9 #include "base/i18n/icu_string_conversions.h" 10 #include "base/logging.h" 11 #include "base/md5.h" 12 #include "base/rand_util.h" 13 #include "base/strings/string_util.h" 14 #include "base/strings/stringprintf.h" 15 #include "base/strings/utf_string_conversions.h" 16 #include "net/base/net_errors.h" 17 #include "net/base/net_util.h" 18 #include "net/http/http_auth.h" 19 #include "net/http/http_request_info.h" 20 #include "net/http/http_util.h" 21 22 namespace net { 23 24 // Digest authentication is specified in RFC 2617. 25 // The expanded derivations are listed in the tables below. 26 27 //==========+==========+==========================================+ 28 // qop |algorithm | response | 29 //==========+==========+==========================================+ 30 // ? | ?, md5, | MD5(MD5(A1):nonce:MD5(A2)) | 31 // | md5-sess | | 32 //--------- +----------+------------------------------------------+ 33 // auth, | ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) | 34 // auth-int | md5-sess | | 35 //==========+==========+==========================================+ 36 // qop |algorithm | A1 | 37 //==========+==========+==========================================+ 38 // | ?, md5 | user:realm:password | 39 //----------+----------+------------------------------------------+ 40 // | md5-sess | MD5(user:realm:password):nonce:cnonce | 41 //==========+==========+==========================================+ 42 // qop |algorithm | A2 | 43 //==========+==========+==========================================+ 44 // ?, auth | | req-method:req-uri | 45 //----------+----------+------------------------------------------+ 46 // auth-int | | req-method:req-uri:MD5(req-entity-body) | 47 //=====================+==========================================+ 48 49 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() { 50 } 51 52 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() { 53 } 54 55 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() { 56 } 57 58 std::string HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce() 59 const { 60 // This is how mozilla generates their cnonce -- a 16 digit hex string. 61 static const char domain[] = "0123456789abcdef"; 62 std::string cnonce; 63 cnonce.reserve(16); 64 for (int i = 0; i < 16; ++i) 65 cnonce.push_back(domain[base::RandInt(0, 15)]); 66 return cnonce; 67 } 68 69 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator( 70 const std::string& nonce) 71 : nonce_(nonce) { 72 } 73 74 std::string HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const { 75 return nonce_; 76 } 77 78 HttpAuthHandlerDigest::Factory::Factory() 79 : nonce_generator_(new DynamicNonceGenerator()) { 80 } 81 82 HttpAuthHandlerDigest::Factory::~Factory() { 83 } 84 85 void HttpAuthHandlerDigest::Factory::set_nonce_generator( 86 const NonceGenerator* nonce_generator) { 87 nonce_generator_.reset(nonce_generator); 88 } 89 90 int HttpAuthHandlerDigest::Factory::CreateAuthHandler( 91 HttpAuth::ChallengeTokenizer* challenge, 92 HttpAuth::Target target, 93 const GURL& origin, 94 CreateReason reason, 95 int digest_nonce_count, 96 const BoundNetLog& net_log, 97 scoped_ptr<HttpAuthHandler>* handler) { 98 // TODO(cbentzel): Move towards model of parsing in the factory 99 // method and only constructing when valid. 100 scoped_ptr<HttpAuthHandler> tmp_handler( 101 new HttpAuthHandlerDigest(digest_nonce_count, nonce_generator_.get())); 102 if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log)) 103 return ERR_INVALID_RESPONSE; 104 handler->swap(tmp_handler); 105 return OK; 106 } 107 108 HttpAuth::AuthorizationResult HttpAuthHandlerDigest::HandleAnotherChallenge( 109 HttpAuth::ChallengeTokenizer* challenge) { 110 // Even though Digest is not connection based, a "second round" is parsed 111 // to differentiate between stale and rejected responses. 112 // Note that the state of the current handler is not mutated - this way if 113 // there is a rejection the realm hasn't changed. 114 if (!LowerCaseEqualsASCII(challenge->scheme(), "digest")) 115 return HttpAuth::AUTHORIZATION_RESULT_INVALID; 116 117 HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs(); 118 119 // Try to find the "stale" value, and also keep track of the realm 120 // for the new challenge. 121 std::string original_realm; 122 while (parameters.GetNext()) { 123 if (LowerCaseEqualsASCII(parameters.name(), "stale")) { 124 if (LowerCaseEqualsASCII(parameters.value(), "true")) 125 return HttpAuth::AUTHORIZATION_RESULT_STALE; 126 } else if (LowerCaseEqualsASCII(parameters.name(), "realm")) { 127 original_realm = parameters.value(); 128 } 129 } 130 return (original_realm_ != original_realm) ? 131 HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM : 132 HttpAuth::AUTHORIZATION_RESULT_REJECT; 133 } 134 135 bool HttpAuthHandlerDigest::Init(HttpAuth::ChallengeTokenizer* challenge) { 136 return ParseChallenge(challenge); 137 } 138 139 int HttpAuthHandlerDigest::GenerateAuthTokenImpl( 140 const AuthCredentials* credentials, const HttpRequestInfo* request, 141 const CompletionCallback& callback, std::string* auth_token) { 142 // Generate a random client nonce. 143 std::string cnonce = nonce_generator_->GenerateNonce(); 144 145 // Extract the request method and path -- the meaning of 'path' is overloaded 146 // in certain cases, to be a hostname. 147 std::string method; 148 std::string path; 149 GetRequestMethodAndPath(request, &method, &path); 150 151 *auth_token = AssembleCredentials(method, path, *credentials, 152 cnonce, nonce_count_); 153 return OK; 154 } 155 156 HttpAuthHandlerDigest::HttpAuthHandlerDigest( 157 int nonce_count, const NonceGenerator* nonce_generator) 158 : stale_(false), 159 algorithm_(ALGORITHM_UNSPECIFIED), 160 qop_(QOP_UNSPECIFIED), 161 nonce_count_(nonce_count), 162 nonce_generator_(nonce_generator) { 163 DCHECK(nonce_generator_); 164 } 165 166 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() { 167 } 168 169 // The digest challenge header looks like: 170 // WWW-Authenticate: Digest 171 // [realm="<realm-value>"] 172 // nonce="<nonce-value>" 173 // [domain="<list-of-URIs>"] 174 // [opaque="<opaque-token-value>"] 175 // [stale="<true-or-false>"] 176 // [algorithm="<digest-algorithm>"] 177 // [qop="<list-of-qop-values>"] 178 // [<extension-directive>] 179 // 180 // Note that according to RFC 2617 (section 1.2) the realm is required. 181 // However we allow it to be omitted, in which case it will default to the 182 // empty string. 183 // 184 // This allowance is for better compatibility with webservers that fail to 185 // send the realm (See http://crbug.com/20984 for an instance where a 186 // webserver was not sending the realm with a BASIC challenge). 187 bool HttpAuthHandlerDigest::ParseChallenge( 188 HttpAuth::ChallengeTokenizer* challenge) { 189 auth_scheme_ = HttpAuth::AUTH_SCHEME_DIGEST; 190 score_ = 2; 191 properties_ = ENCRYPTS_IDENTITY; 192 193 // Initialize to defaults. 194 stale_ = false; 195 algorithm_ = ALGORITHM_UNSPECIFIED; 196 qop_ = QOP_UNSPECIFIED; 197 realm_ = original_realm_ = nonce_ = domain_ = opaque_ = std::string(); 198 199 // FAIL -- Couldn't match auth-scheme. 200 if (!LowerCaseEqualsASCII(challenge->scheme(), "digest")) 201 return false; 202 203 HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs(); 204 205 // Loop through all the properties. 206 while (parameters.GetNext()) { 207 // FAIL -- couldn't parse a property. 208 if (!ParseChallengeProperty(parameters.name(), 209 parameters.value())) 210 return false; 211 } 212 213 // Check if tokenizer failed. 214 if (!parameters.valid()) 215 return false; 216 217 // Check that a minimum set of properties were provided. 218 if (nonce_.empty()) 219 return false; 220 221 return true; 222 } 223 224 bool HttpAuthHandlerDigest::ParseChallengeProperty(const std::string& name, 225 const std::string& value) { 226 if (LowerCaseEqualsASCII(name, "realm")) { 227 std::string realm; 228 if (!base::ConvertToUtf8AndNormalize(value, base::kCodepageLatin1, &realm)) 229 return false; 230 realm_ = realm; 231 original_realm_ = value; 232 } else if (LowerCaseEqualsASCII(name, "nonce")) { 233 nonce_ = value; 234 } else if (LowerCaseEqualsASCII(name, "domain")) { 235 domain_ = value; 236 } else if (LowerCaseEqualsASCII(name, "opaque")) { 237 opaque_ = value; 238 } else if (LowerCaseEqualsASCII(name, "stale")) { 239 // Parse the stale boolean. 240 stale_ = LowerCaseEqualsASCII(value, "true"); 241 } else if (LowerCaseEqualsASCII(name, "algorithm")) { 242 // Parse the algorithm. 243 if (LowerCaseEqualsASCII(value, "md5")) { 244 algorithm_ = ALGORITHM_MD5; 245 } else if (LowerCaseEqualsASCII(value, "md5-sess")) { 246 algorithm_ = ALGORITHM_MD5_SESS; 247 } else { 248 DVLOG(1) << "Unknown value of algorithm"; 249 return false; // FAIL -- unsupported value of algorithm. 250 } 251 } else if (LowerCaseEqualsASCII(name, "qop")) { 252 // Parse the comma separated list of qops. 253 // auth is the only supported qop, and all other values are ignored. 254 HttpUtil::ValuesIterator qop_values(value.begin(), value.end(), ','); 255 qop_ = QOP_UNSPECIFIED; 256 while (qop_values.GetNext()) { 257 if (LowerCaseEqualsASCII(qop_values.value(), "auth")) { 258 qop_ = QOP_AUTH; 259 break; 260 } 261 } 262 } else { 263 DVLOG(1) << "Skipping unrecognized digest property"; 264 // TODO(eroman): perhaps we should fail instead of silently skipping? 265 } 266 return true; 267 } 268 269 // static 270 std::string HttpAuthHandlerDigest::QopToString(QualityOfProtection qop) { 271 switch (qop) { 272 case QOP_UNSPECIFIED: 273 return std::string(); 274 case QOP_AUTH: 275 return "auth"; 276 default: 277 NOTREACHED(); 278 return std::string(); 279 } 280 } 281 282 // static 283 std::string HttpAuthHandlerDigest::AlgorithmToString( 284 DigestAlgorithm algorithm) { 285 switch (algorithm) { 286 case ALGORITHM_UNSPECIFIED: 287 return std::string(); 288 case ALGORITHM_MD5: 289 return "MD5"; 290 case ALGORITHM_MD5_SESS: 291 return "MD5-sess"; 292 default: 293 NOTREACHED(); 294 return std::string(); 295 } 296 } 297 298 void HttpAuthHandlerDigest::GetRequestMethodAndPath( 299 const HttpRequestInfo* request, 300 std::string* method, 301 std::string* path) const { 302 DCHECK(request); 303 304 const GURL& url = request->url; 305 306 if (target_ == HttpAuth::AUTH_PROXY && 307 (url.SchemeIs("https") || url.SchemeIs("ws") || url.SchemeIs("wss"))) { 308 *method = "CONNECT"; 309 *path = GetHostAndPort(url); 310 } else { 311 *method = request->method; 312 *path = HttpUtil::PathForRequest(url); 313 } 314 } 315 316 std::string HttpAuthHandlerDigest::AssembleResponseDigest( 317 const std::string& method, 318 const std::string& path, 319 const AuthCredentials& credentials, 320 const std::string& cnonce, 321 const std::string& nc) const { 322 // ha1 = MD5(A1) 323 // TODO(eroman): is this the right encoding? 324 std::string ha1 = base::MD5String(UTF16ToUTF8(credentials.username()) + ":" + 325 original_realm_ + ":" + 326 UTF16ToUTF8(credentials.password())); 327 if (algorithm_ == HttpAuthHandlerDigest::ALGORITHM_MD5_SESS) 328 ha1 = base::MD5String(ha1 + ":" + nonce_ + ":" + cnonce); 329 330 // ha2 = MD5(A2) 331 // TODO(eroman): need to add MD5(req-entity-body) for qop=auth-int. 332 std::string ha2 = base::MD5String(method + ":" + path); 333 334 std::string nc_part; 335 if (qop_ != HttpAuthHandlerDigest::QOP_UNSPECIFIED) { 336 nc_part = nc + ":" + cnonce + ":" + QopToString(qop_) + ":"; 337 } 338 339 return base::MD5String(ha1 + ":" + nonce_ + ":" + nc_part + ha2); 340 } 341 342 std::string HttpAuthHandlerDigest::AssembleCredentials( 343 const std::string& method, 344 const std::string& path, 345 const AuthCredentials& credentials, 346 const std::string& cnonce, 347 int nonce_count) const { 348 // the nonce-count is an 8 digit hex string. 349 std::string nc = base::StringPrintf("%08x", nonce_count); 350 351 // TODO(eroman): is this the right encoding? 352 std::string authorization = (std::string("Digest username=") + 353 HttpUtil::Quote( 354 UTF16ToUTF8(credentials.username()))); 355 authorization += ", realm=" + HttpUtil::Quote(original_realm_); 356 authorization += ", nonce=" + HttpUtil::Quote(nonce_); 357 authorization += ", uri=" + HttpUtil::Quote(path); 358 359 if (algorithm_ != ALGORITHM_UNSPECIFIED) { 360 authorization += ", algorithm=" + AlgorithmToString(algorithm_); 361 } 362 std::string response = AssembleResponseDigest(method, path, credentials, 363 cnonce, nc); 364 // No need to call HttpUtil::Quote() as the response digest cannot contain 365 // any characters needing to be escaped. 366 authorization += ", response=\"" + response + "\""; 367 368 if (!opaque_.empty()) { 369 authorization += ", opaque=" + HttpUtil::Quote(opaque_); 370 } 371 if (qop_ != QOP_UNSPECIFIED) { 372 // TODO(eroman): Supposedly IIS server requires quotes surrounding qop. 373 authorization += ", qop=" + QopToString(qop_); 374 authorization += ", nc=" + nc; 375 authorization += ", cnonce=" + HttpUtil::Quote(cnonce); 376 } 377 378 return authorization; 379 } 380 381 } // namespace net 382