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