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