• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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