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