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