• 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 #ifdef UNSAFE_BUFFERS_BUILD
6 // TODO(crbug.com/40284755): Remove this and spanify to fix the errors.
7 #pragma allow_unsafe_buffers
8 #endif
9 
10 #include "net/http/http_auth_handler_digest.h"
11 
12 #include <string>
13 #include <string_view>
14 
15 #include "base/hash/md5.h"
16 #include "base/logging.h"
17 #include "base/memory/ptr_util.h"
18 #include "base/rand_util.h"
19 #include "base/strings/string_number_conversions.h"
20 #include "base/strings/string_util.h"
21 #include "base/strings/stringprintf.h"
22 #include "base/strings/utf_string_conversions.h"
23 #include "net/base/features.h"
24 #include "net/base/net_errors.h"
25 #include "net/base/net_string_util.h"
26 #include "net/base/url_util.h"
27 #include "net/dns/host_resolver.h"
28 #include "net/http/http_auth.h"
29 #include "net/http/http_auth_challenge_tokenizer.h"
30 #include "net/http/http_auth_scheme.h"
31 #include "net/http/http_request_info.h"
32 #include "net/http/http_util.h"
33 #include "third_party/boringssl/src/include/openssl/digest.h"
34 #include "url/gurl.h"
35 
36 namespace net {
37 
38 // Digest authentication is specified in RFC 7616.
39 // The expanded derivations for algorithm=MD5 are listed in the tables below.
40 
41 //==========+==========+==========================================+
42 //    qop   |algorithm |               response                   |
43 //==========+==========+==========================================+
44 //    ?     |  ?, md5, | MD5(MD5(A1):nonce:MD5(A2))               |
45 //          | md5-sess |                                          |
46 //--------- +----------+------------------------------------------+
47 //   auth,  |  ?, md5, | MD5(MD5(A1):nonce:nc:cnonce:qop:MD5(A2)) |
48 // auth-int | md5-sess |                                          |
49 //==========+==========+==========================================+
50 //    qop   |algorithm |                  A1                      |
51 //==========+==========+==========================================+
52 //          | ?, md5   | user:realm:password                      |
53 //----------+----------+------------------------------------------+
54 //          | md5-sess | MD5(user:realm:password):nonce:cnonce    |
55 //==========+==========+==========================================+
56 //    qop   |algorithm |                  A2                      |
57 //==========+==========+==========================================+
58 //  ?, auth |          | req-method:req-uri                       |
59 //----------+----------+------------------------------------------+
60 // auth-int |          | req-method:req-uri:MD5(req-entity-body)  |
61 //=====================+==========================================+
62 
63 HttpAuthHandlerDigest::NonceGenerator::NonceGenerator() = default;
64 
65 HttpAuthHandlerDigest::NonceGenerator::~NonceGenerator() = default;
66 
67 HttpAuthHandlerDigest::DynamicNonceGenerator::DynamicNonceGenerator() = default;
68 
GenerateNonce() const69 std::string HttpAuthHandlerDigest::DynamicNonceGenerator::GenerateNonce()
70     const {
71   // This is how mozilla generates their cnonce -- a 16 digit hex string.
72   static const char domain[] = "0123456789abcdef";
73   std::string cnonce;
74   cnonce.reserve(16);
75   for (int i = 0; i < 16; ++i) {
76     cnonce.push_back(domain[base::RandInt(0, 15)]);
77   }
78   return cnonce;
79 }
80 
FixedNonceGenerator(const std::string & nonce)81 HttpAuthHandlerDigest::FixedNonceGenerator::FixedNonceGenerator(
82     const std::string& nonce)
83     : nonce_(nonce) {}
84 
GenerateNonce() const85 std::string HttpAuthHandlerDigest::FixedNonceGenerator::GenerateNonce() const {
86   return nonce_;
87 }
88 
Factory()89 HttpAuthHandlerDigest::Factory::Factory()
90     : nonce_generator_(std::make_unique<DynamicNonceGenerator>()) {}
91 
92 HttpAuthHandlerDigest::Factory::~Factory() = default;
93 
set_nonce_generator(std::unique_ptr<const NonceGenerator> nonce_generator)94 void HttpAuthHandlerDigest::Factory::set_nonce_generator(
95     std::unique_ptr<const NonceGenerator> nonce_generator) {
96   nonce_generator_ = std::move(nonce_generator);
97 }
98 
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)99 int HttpAuthHandlerDigest::Factory::CreateAuthHandler(
100     HttpAuthChallengeTokenizer* challenge,
101     HttpAuth::Target target,
102     const SSLInfo& ssl_info,
103     const NetworkAnonymizationKey& network_anonymization_key,
104     const url::SchemeHostPort& scheme_host_port,
105     CreateReason reason,
106     int digest_nonce_count,
107     const NetLogWithSource& net_log,
108     HostResolver* host_resolver,
109     std::unique_ptr<HttpAuthHandler>* handler) {
110   // TODO(cbentzel): Move towards model of parsing in the factory
111   //                 method and only constructing when valid.
112   auto tmp_handler = base::WrapUnique(
113       new HttpAuthHandlerDigest(digest_nonce_count, nonce_generator_.get()));
114   if (!tmp_handler->InitFromChallenge(challenge, target, ssl_info,
115                                       network_anonymization_key,
116                                       scheme_host_port, net_log)) {
117     return ERR_INVALID_RESPONSE;
118   }
119   *handler = std::move(tmp_handler);
120   return OK;
121 }
122 
Init(HttpAuthChallengeTokenizer * challenge,const SSLInfo & ssl_info,const NetworkAnonymizationKey & network_anonymization_key)123 bool HttpAuthHandlerDigest::Init(
124     HttpAuthChallengeTokenizer* challenge,
125     const SSLInfo& ssl_info,
126     const NetworkAnonymizationKey& network_anonymization_key) {
127   return ParseChallenge(challenge);
128 }
129 
GenerateAuthTokenImpl(const AuthCredentials * credentials,const HttpRequestInfo * request,CompletionOnceCallback callback,std::string * auth_token)130 int HttpAuthHandlerDigest::GenerateAuthTokenImpl(
131     const AuthCredentials* credentials,
132     const HttpRequestInfo* request,
133     CompletionOnceCallback callback,
134     std::string* auth_token) {
135   // Generate a random client nonce.
136   std::string cnonce = nonce_generator_->GenerateNonce();
137 
138   // Extract the request method and path -- the meaning of 'path' is overloaded
139   // in certain cases, to be a hostname.
140   std::string method;
141   std::string path;
142   GetRequestMethodAndPath(request, &method, &path);
143 
144   *auth_token =
145       AssembleCredentials(method, path, *credentials, cnonce, nonce_count_);
146   return OK;
147 }
148 
HandleAnotherChallengeImpl(HttpAuthChallengeTokenizer * challenge)149 HttpAuth::AuthorizationResult HttpAuthHandlerDigest::HandleAnotherChallengeImpl(
150     HttpAuthChallengeTokenizer* challenge) {
151   // Even though Digest is not connection based, a "second round" is parsed
152   // to differentiate between stale and rejected responses.
153   // Note that the state of the current handler is not mutated - this way if
154   // there is a rejection the realm hasn't changed.
155   if (challenge->auth_scheme() != kDigestAuthScheme) {
156     return HttpAuth::AUTHORIZATION_RESULT_INVALID;
157   }
158 
159   HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs();
160 
161   // Try to find the "stale" value, and also keep track of the realm
162   // for the new challenge.
163   std::string original_realm;
164   while (parameters.GetNext()) {
165     if (base::EqualsCaseInsensitiveASCII(parameters.name(), "stale")) {
166       if (base::EqualsCaseInsensitiveASCII(parameters.value(), "true")) {
167         return HttpAuth::AUTHORIZATION_RESULT_STALE;
168       }
169     } else if (base::EqualsCaseInsensitiveASCII(parameters.name(), "realm")) {
170       // This has to be a copy, since value_piece() may point to an internal
171       // buffer of `parameters`.
172       original_realm = parameters.value();
173     }
174   }
175   return (original_realm_ != original_realm)
176              ? HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM
177              : HttpAuth::AUTHORIZATION_RESULT_REJECT;
178 }
179 
HttpAuthHandlerDigest(int nonce_count,const NonceGenerator * nonce_generator)180 HttpAuthHandlerDigest::HttpAuthHandlerDigest(
181     int nonce_count,
182     const NonceGenerator* nonce_generator)
183     : nonce_count_(nonce_count), nonce_generator_(nonce_generator) {
184   DCHECK(nonce_generator_);
185 }
186 
187 HttpAuthHandlerDigest::~HttpAuthHandlerDigest() = default;
188 
189 // The digest challenge header looks like:
190 //   WWW-Authenticate: Digest
191 //     [realm="<realm-value>"]
192 //     nonce="<nonce-value>"
193 //     [domain="<list-of-URIs>"]
194 //     [opaque="<opaque-token-value>"]
195 //     [stale="<true-or-false>"]
196 //     [algorithm="<digest-algorithm>"]
197 //     [qop="<list-of-qop-values>"]
198 //     [<extension-directive>]
199 //
200 // Note that according to RFC 2617 (section 1.2) the realm is required.
201 // However we allow it to be omitted, in which case it will default to the
202 // empty string.
203 //
204 // This allowance is for better compatibility with webservers that fail to
205 // send the realm (See http://crbug.com/20984 for an instance where a
206 // webserver was not sending the realm with a BASIC challenge).
ParseChallenge(HttpAuthChallengeTokenizer * challenge)207 bool HttpAuthHandlerDigest::ParseChallenge(
208     HttpAuthChallengeTokenizer* challenge) {
209   auth_scheme_ = HttpAuth::AUTH_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_ = original_realm_ = nonce_ = domain_ = opaque_ = std::string();
218 
219   // FAIL -- Couldn't match auth-scheme.
220   if (challenge->auth_scheme() != kDigestAuthScheme) {
221     return false;
222   }
223 
224   HttpUtil::NameValuePairsIterator parameters = challenge->param_pairs();
225 
226   // Loop through all the properties.
227   while (parameters.GetNext()) {
228     // FAIL -- couldn't parse a property.
229     if (!ParseChallengeProperty(parameters.name(), parameters.value())) {
230       return false;
231     }
232   }
233 
234   // Check if tokenizer failed.
235   if (!parameters.valid()) {
236     return false;
237   }
238 
239   // Check that a minimum set of properties were provided.
240   if (nonce_.empty()) {
241     return false;
242   }
243 
244   return true;
245 }
246 
ParseChallengeProperty(std::string_view name,std::string_view value)247 bool HttpAuthHandlerDigest::ParseChallengeProperty(std::string_view name,
248                                                    std::string_view value) {
249   if (base::EqualsCaseInsensitiveASCII(name, "realm")) {
250     std::string realm;
251     if (!ConvertToUtf8AndNormalize(value, kCharsetLatin1, &realm)) {
252       return false;
253     }
254     realm_ = realm;
255     original_realm_ = std::string(value);
256   } else if (base::EqualsCaseInsensitiveASCII(name, "nonce")) {
257     nonce_ = std::string(value);
258   } else if (base::EqualsCaseInsensitiveASCII(name, "domain")) {
259     domain_ = std::string(value);
260   } else if (base::EqualsCaseInsensitiveASCII(name, "opaque")) {
261     opaque_ = std::string(value);
262   } else if (base::EqualsCaseInsensitiveASCII(name, "stale")) {
263     // Parse the stale boolean.
264     stale_ = base::EqualsCaseInsensitiveASCII(value, "true");
265   } else if (base::EqualsCaseInsensitiveASCII(name, "algorithm")) {
266     // Parse the algorithm.
267     if (base::EqualsCaseInsensitiveASCII(value, "md5")) {
268       algorithm_ = Algorithm::MD5;
269     } else if (base::EqualsCaseInsensitiveASCII(value, "md5-sess")) {
270       algorithm_ = Algorithm::MD5_SESS;
271     } else if (base::EqualsCaseInsensitiveASCII(value, "sha-256")) {
272       algorithm_ = Algorithm::SHA256;
273     } else if (base::EqualsCaseInsensitiveASCII(value, "sha-256-sess")) {
274       algorithm_ = Algorithm::SHA256_SESS;
275     } else {
276       DVLOG(1) << "Unknown value of algorithm";
277       return false;  // FAIL -- unsupported value of algorithm.
278     }
279   } else if (base::EqualsCaseInsensitiveASCII(name, "userhash")) {
280     userhash_ = base::EqualsCaseInsensitiveASCII(value, "true");
281   } else if (base::EqualsCaseInsensitiveASCII(name, "qop")) {
282     // Parse the comma separated list of qops.
283     // auth is the only supported qop, and all other values are ignored.
284     HttpUtil::ValuesIterator qop_values(value, /*delimiter=*/',');
285     qop_ = QOP_UNSPECIFIED;
286     while (qop_values.GetNext()) {
287       if (base::EqualsCaseInsensitiveASCII(qop_values.value(), "auth")) {
288         qop_ = QOP_AUTH;
289         break;
290       }
291     }
292   } else {
293     DVLOG(1) << "Skipping unrecognized digest property";
294     // TODO(eroman): perhaps we should fail instead of silently skipping?
295   }
296 
297   return true;
298 }
299 
300 // static
QopToString(QualityOfProtection qop)301 std::string HttpAuthHandlerDigest::QopToString(QualityOfProtection qop) {
302   switch (qop) {
303     case QOP_UNSPECIFIED:
304       return std::string();
305     case QOP_AUTH:
306       return "auth";
307     default:
308       NOTREACHED();
309   }
310 }
311 
312 // static
AlgorithmToString(Algorithm algorithm)313 std::string HttpAuthHandlerDigest::AlgorithmToString(Algorithm algorithm) {
314   switch (algorithm) {
315     case Algorithm::UNSPECIFIED:
316       return std::string();
317     case Algorithm::MD5:
318       return "MD5";
319     case Algorithm::MD5_SESS:
320       return "MD5-sess";
321     case Algorithm::SHA256:
322       return "SHA-256";
323     case Algorithm::SHA256_SESS:
324       return "SHA-256-sess";
325     default:
326       NOTREACHED();
327   }
328 }
329 
GetRequestMethodAndPath(const HttpRequestInfo * request,std::string * method,std::string * path) const330 void HttpAuthHandlerDigest::GetRequestMethodAndPath(
331     const HttpRequestInfo* request,
332     std::string* method,
333     std::string* path) const {
334   DCHECK(request);
335 
336   const GURL& url = request->url;
337 
338   if (target_ == HttpAuth::AUTH_PROXY &&
339       (url.SchemeIs("https") || url.SchemeIsWSOrWSS())) {
340     *method = "CONNECT";
341     *path = GetHostAndPort(url);
342   } else {
343     *method = request->method;
344     *path = url.PathForRequest();
345   }
346 }
347 
348 class HttpAuthHandlerDigest::DigestContext {
349  public:
DigestContext(HttpAuthHandlerDigest::Algorithm algo)350   explicit DigestContext(HttpAuthHandlerDigest::Algorithm algo) {
351     switch (algo) {
352       case HttpAuthHandlerDigest::Algorithm::MD5:
353       case HttpAuthHandlerDigest::Algorithm::MD5_SESS:
354       case HttpAuthHandlerDigest::Algorithm::UNSPECIFIED:
355         CHECK(EVP_DigestInit(md_ctx_.get(), EVP_md5()));
356         out_len_ = 16;
357         break;
358       case HttpAuthHandlerDigest::Algorithm::SHA256:
359       case HttpAuthHandlerDigest::Algorithm::SHA256_SESS:
360         CHECK(EVP_DigestInit(md_ctx_.get(), EVP_sha256()));
361         out_len_ = 32;
362         break;
363     }
364   }
Update(std::string_view s)365   void Update(std::string_view s) {
366     CHECK(EVP_DigestUpdate(md_ctx_.get(), s.data(), s.size()));
367   }
Update(std::initializer_list<std::string_view> sps)368   void Update(std::initializer_list<std::string_view> sps) {
369     for (const auto sp : sps) {
370       Update(sp);
371     }
372   }
HexDigest()373   std::string HexDigest() {
374     uint8_t md_value[EVP_MAX_MD_SIZE] = {};
375     unsigned int md_len = sizeof(md_value);
376     CHECK(EVP_DigestFinal_ex(md_ctx_.get(), md_value, &md_len));
377     return base::ToLowerASCII(
378         base::HexEncode(base::span(md_value).first(out_len_)));
379   }
380 
381  private:
382   bssl::ScopedEVP_MD_CTX md_ctx_;
383   size_t out_len_;
384 };
385 
AssembleResponseDigest(const std::string & method,const std::string & path,const AuthCredentials & credentials,const std::string & cnonce,const std::string & nc) const386 std::string HttpAuthHandlerDigest::AssembleResponseDigest(
387     const std::string& method,
388     const std::string& path,
389     const AuthCredentials& credentials,
390     const std::string& cnonce,
391     const std::string& nc) const {
392   // ha1 = H(A1)
393   DigestContext ha1_ctx(algorithm_);
394   ha1_ctx.Update({base::UTF16ToUTF8(credentials.username()), ":",
395                   original_realm_, ":",
396                   base::UTF16ToUTF8(credentials.password())});
397   std::string ha1 = ha1_ctx.HexDigest();
398 
399   if (algorithm_ == HttpAuthHandlerDigest::Algorithm::MD5_SESS ||
400       algorithm_ == HttpAuthHandlerDigest::Algorithm::SHA256_SESS) {
401     DigestContext sess_ctx(algorithm_);
402     sess_ctx.Update({ha1, ":", nonce_, ":", cnonce});
403     ha1 = sess_ctx.HexDigest();
404   }
405 
406   // ha2 = H(A2)
407   // TODO(eroman): need to add H(req-entity-body) for qop=auth-int.
408   DigestContext ha2_ctx(algorithm_);
409   ha2_ctx.Update({method, ":", path});
410   const std::string ha2 = ha2_ctx.HexDigest();
411 
412   DigestContext resp_ctx(algorithm_);
413   resp_ctx.Update({ha1, ":", nonce_, ":"});
414 
415   if (qop_ != HttpAuthHandlerDigest::QOP_UNSPECIFIED) {
416     resp_ctx.Update({nc, ":", cnonce, ":", QopToString(qop_), ":"});
417   }
418 
419   resp_ctx.Update(ha2);
420 
421   return resp_ctx.HexDigest();
422 }
423 
AssembleCredentials(const std::string & method,const std::string & path,const AuthCredentials & credentials,const std::string & cnonce,int nonce_count) const424 std::string HttpAuthHandlerDigest::AssembleCredentials(
425     const std::string& method,
426     const std::string& path,
427     const AuthCredentials& credentials,
428     const std::string& cnonce,
429     int nonce_count) const {
430   // the nonce-count is an 8 digit hex string.
431   std::string nc = base::StringPrintf("%08x", nonce_count);
432 
433   // TODO(eroman): is this the right encoding?
434   std::string username = base::UTF16ToUTF8(credentials.username());
435   if (userhash_) {  // https://www.rfc-editor.org/rfc/rfc7616#section-3.4.4
436     DigestContext uh_ctx(algorithm_);
437     uh_ctx.Update({username, ":", realm_});
438     username = uh_ctx.HexDigest();
439   }
440 
441   std::string authorization =
442       (std::string("Digest username=") + HttpUtil::Quote(username));
443   authorization += ", realm=" + HttpUtil::Quote(original_realm_);
444   authorization += ", nonce=" + HttpUtil::Quote(nonce_);
445   authorization += ", uri=" + HttpUtil::Quote(path);
446 
447   if (algorithm_ != Algorithm::UNSPECIFIED) {
448     authorization += ", algorithm=" + AlgorithmToString(algorithm_);
449   }
450   std::string response =
451       AssembleResponseDigest(method, path, credentials, cnonce, nc);
452   // No need to call HttpUtil::Quote() as the response digest cannot contain
453   // any characters needing to be escaped.
454   authorization += ", response=\"" + response + "\"";
455 
456   if (!opaque_.empty()) {
457     authorization += ", opaque=" + HttpUtil::Quote(opaque_);
458   }
459   if (qop_ != QOP_UNSPECIFIED) {
460     // TODO(eroman): Supposedly IIS server requires quotes surrounding qop.
461     authorization += ", qop=" + QopToString(qop_);
462     authorization += ", nc=" + nc;
463     authorization += ", cnonce=" + HttpUtil::Quote(cnonce);
464   }
465   if (userhash_) {
466     authorization += ", userhash=true";
467   }
468 
469   return authorization;
470 }
471 
472 }  // namespace net
473