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