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