]> git.ipfire.org Git - thirdparty/squid.git/blame - src/auth/digest/Config.cc
Bug 5301: cachemgr.cgi not showing new manager interface URLs (#1479)
[thirdparty/squid.git] / src / auth / digest / Config.cc
CommitLineData
2d70df72 1/*
b8ae064d 2 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
2d70df72 3 *
bbc27441
AJ
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
2d70df72 7 */
8
bbc27441
AJ
9/* DEBUG: section 29 Authenticator */
10
2d70df72 11/* The functions in this file handle authentication.
12 * They DO NOT perform access control or auditing.
13 * See acl.c for access control and client_side.c for auditing */
14
582c2af2 15#include "squid.h"
fde785ee 16#include "auth/CredentialsCache.h"
12daeef6 17#include "auth/digest/Config.h"
616cfc4c 18#include "auth/digest/Scheme.h"
aa110616 19#include "auth/digest/User.h"
616cfc4c 20#include "auth/digest/UserRequest.h"
3ad63615 21#include "auth/Gadgets.h"
aa110616 22#include "auth/State.h"
7e851a3e 23#include "auth/toUtf.h"
a0655385 24#include "base/LookupTable.h"
4a28fc55 25#include "base/Random.h"
602d9612 26#include "cache_cf.h"
a553a5a3 27#include "event.h"
24438ec5 28#include "helper.h"
a5bac1d2 29#include "HttpHeaderTools.h"
924f73bc 30#include "HttpReply.h"
602d9612 31#include "HttpRequest.h"
b20ce974 32#include "md5.h"
602d9612
A
33#include "mgr/Registration.h"
34#include "rfc2617.h"
65e41a45 35#include "sbuf/SBuf.h"
7e851a3e 36#include "sbuf/StringConvert.h"
602d9612 37#include "Store.h"
28204b3b 38#include "StrList.h"
602d9612 39#include "wordlist.h"
c78aa667 40
ed6e9fb9
AJ
41/* digest_nonce_h still uses explicit alloc()/freeOne() MemPool calls.
42 * XXX: convert to MEMPROXY_CLASS() API
43 */
7ae0a0c5 44#include "mem/Allocator.h"
ed6e9fb9 45#include "mem/Pool.h"
2d70df72 46
2d70df72 47static AUTHSSTATS authenticateDigestStats;
2d70df72 48
3bd118d6 49Helper::ClientPointer digestauthenticators;
2d70df72 50
51static hash_table *digest_nonce_cache;
52
2d70df72 53static int authdigest_initialised = 0;
341876ec 54static Mem::Allocator *digest_nonce_pool = nullptr;
2d70df72 55
9abd1514 56enum http_digest_attr_type {
d6b7a3c4 57 DIGEST_USERNAME,
cb14509d
HN
58 DIGEST_REALM,
59 DIGEST_QOP,
60 DIGEST_ALGORITHM,
61 DIGEST_URI,
62 DIGEST_NONCE,
63 DIGEST_NC,
64 DIGEST_CNONCE,
65 DIGEST_RESPONSE,
ae22f65a 66 DIGEST_INVALID_ATTR
a15e94ec 67};
a0655385
FC
68
69static const LookupTable<http_digest_attr_type>::Record
4be4fedc 70DigestAttrs[] = {
a0655385
FC
71 {"username", DIGEST_USERNAME},
72 {"realm", DIGEST_REALM},
73 {"qop", DIGEST_QOP},
74 {"algorithm", DIGEST_ALGORITHM},
75 {"uri", DIGEST_URI},
76 {"nonce", DIGEST_NONCE},
77 {"nc", DIGEST_NC},
78 {"cnonce", DIGEST_CNONCE},
79 {"response", DIGEST_RESPONSE},
ae22f65a 80 {nullptr, DIGEST_INVALID_ATTR}
a0655385
FC
81};
82
83LookupTable<http_digest_attr_type>
f5ef79c0 84DigestFieldsLookupTable(DIGEST_INVALID_ATTR, DigestAttrs);
a0655385 85
2d70df72 86/*
87 *
88 * Nonce Functions
89 *
90 */
91
92static void authenticateDigestNonceCacheCleanup(void *data);
b20ce974 93static digest_nonce_h *authenticateDigestNonceFindNonce(const char *noncehex);
c78aa667 94static void authenticateDigestNonceDelete(digest_nonce_h * nonce);
c193c972 95static void authenticateDigestNonceSetup(void);
c78aa667 96static void authDigestNonceEncode(digest_nonce_h * nonce);
c78aa667 97static void authDigestNonceLink(digest_nonce_h * nonce);
c78aa667 98static void authDigestNonceUserUnlink(digest_nonce_h * nonce);
2d70df72 99
c78aa667 100static void
2d70df72 101authDigestNonceEncode(digest_nonce_h * nonce)
102{
103 if (!nonce)
62e76326 104 return;
105
4a8b20e8 106 if (nonce->key)
62e76326 107 xfree(nonce->key);
108
b20ce974 109 SquidMD5_CTX Md5Ctx;
110 HASH H;
111 SquidMD5Init(&Md5Ctx);
112 SquidMD5Update(&Md5Ctx, reinterpret_cast<const uint8_t *>(&nonce->noncedata), sizeof(nonce->noncedata));
113 SquidMD5Final(reinterpret_cast<uint8_t *>(H), &Md5Ctx);
114
115 nonce->key = xcalloc(sizeof(HASHHEX), 1);
116 CvtHex(H, static_cast<char *>(nonce->key));
2d70df72 117}
118
572d2e31 119digest_nonce_h *
c193c972 120authenticateDigestNonceNew(void)
2d70df72 121{
b001e822 122 digest_nonce_h *newnonce = static_cast < digest_nonce_h * >(digest_nonce_pool->alloc());
2d70df72 123
62e76326 124 /* NONCE CREATION - NOTES AND REASONING. RBC 20010108
125 * === EXCERPT FROM RFC 2617 ===
126 * The contents of the nonce are implementation dependent. The quality
127 * of the implementation depends on a good choice. A nonce might, for
128 * example, be constructed as the base 64 encoding of
26ac0430 129 *
62e76326 130 * time-stamp H(time-stamp ":" ETag ":" private-key)
26ac0430 131 *
62e76326 132 * where time-stamp is a server-generated time or other non-repeating
133 * value, ETag is the value of the HTTP ETag header associated with
134 * the requested entity, and private-key is data known only to the
135 * server. With a nonce of this form a server would recalculate the
136 * hash portion after receiving the client authentication header and
137 * reject the request if it did not match the nonce from that header
138 * or if the time-stamp value is not recent enough. In this way the
139 * server can limit the time of the nonce's validity. The inclusion of
140 * the ETag prevents a replay request for an updated version of the
141 * resource. (Note: including the IP address of the client in the
142 * nonce would appear to offer the server the ability to limit the
143 * reuse of the nonce to the same client that originally got it.
144 * However, that would break proxy farms, where requests from a single
145 * user often go through different proxies in the farm. Also, IP
146 * address spoofing is not that hard.)
147 * ====
26ac0430 148 *
62e76326 149 * Now for my reasoning:
150 * We will not accept a unrecognised nonce->we have all recognisable
b20ce974 151 * nonces stored. If we send out unique encodings we guarantee
62e76326 152 * that a given nonce applies to only one user (barring attacks or
153 * really bad timing with expiry and creation). Using a random
154 * component in the nonce allows us to loop to find a unique nonce.
2f8abb64 155 * We use H(nonce_data) so the nonce is meaningless to the receiver.
b8639683 156 * So our nonce looks like hex(H(timestamp,randomdata))
42df4209 157 * And even if our randomness is not very random we don't really care
b8639683 158 * - the timestamp also guarantees local uniqueness in the input to
159 * the hash function.
62e76326 160 */
4a28fc55 161 static std::mt19937 mt(RandomSeed32());
09835feb 162 static std::uniform_int_distribution<uint32_t> newRandomData;
2d70df72 163
164 /* create a new nonce */
165 newnonce->nc = 0;
3dd52a0b 166 newnonce->flags.valid = true;
2d70df72 167 newnonce->noncedata.creationtime = current_time.tv_sec;
42df4209 168 newnonce->noncedata.randomdata = newRandomData(mt);
2d70df72 169
170 authDigestNonceEncode(newnonce);
62e76326 171
42df4209 172 // ensure temporal uniqueness by checking for existing nonce
3812fb2c 173 while (authenticateDigestNonceFindNonce((char const *) (newnonce->key))) {
62e76326 174 /* create a new nonce */
42df4209 175 newnonce->noncedata.randomdata = newRandomData(mt);
62e76326 176 authDigestNonceEncode(newnonce);
2d70df72 177 }
62e76326 178
4a8b20e8 179 hash_join(digest_nonce_cache, newnonce);
2d70df72 180 /* the cache's link */
181 authDigestNonceLink(newnonce);
3dd52a0b 182 newnonce->flags.incache = true;
a4c3b397 183 debugs(29, 5, "created nonce " << newnonce << " at " << newnonce->noncedata.creationtime);
2d70df72 184 return newnonce;
185}
186
c78aa667 187static void
2d70df72 188authenticateDigestNonceDelete(digest_nonce_h * nonce)
189{
190 if (nonce) {
62e76326 191 assert(nonce->references == 0);
3dd52a0b 192 assert(!nonce->flags.incache);
62e76326 193
194 safe_free(nonce->key);
195
dc47f531 196 digest_nonce_pool->freeOne(nonce);
2d70df72 197 }
198}
199
c78aa667 200static void
c193c972 201authenticateDigestNonceSetup(void)
2d70df72 202{
203 if (!digest_nonce_pool)
04eb0689 204 digest_nonce_pool = memPoolCreate("Digest Scheme nonce's", sizeof(digest_nonce_h));
62e76326 205
2d70df72 206 if (!digest_nonce_cache) {
30abd221 207 digest_nonce_cache = hash_create((HASHCMP *) strcmp, 7921, hash_string);
62e76326 208 assert(digest_nonce_cache);
aee3523a 209 eventAdd("Digest nonce cache maintenance", authenticateDigestNonceCacheCleanup, nullptr, static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->nonceGCInterval, 1);
2d70df72 210 }
211}
212
d6374be6 213void
c193c972 214authenticateDigestNonceShutdown(void)
2d70df72 215{
62e76326 216 /*
2d70df72 217 * We empty the cache of any nonces left in there.
218 */
219 digest_nonce_h *nonce;
62e76326 220
2d70df72 221 if (digest_nonce_cache) {
a4c3b397 222 debugs(29, 2, "Shutting down nonce cache");
62e76326 223 hash_first(digest_nonce_cache);
224
225 while ((nonce = ((digest_nonce_h *) hash_next(digest_nonce_cache)))) {
226 assert(nonce->flags.incache);
227 authDigestNoncePurge(nonce);
228 }
2d70df72 229 }
62e76326 230
a4c3b397 231 debugs(29, 2, "Nonce cache shutdown");
2d70df72 232}
233
c78aa667 234static void
ced8def3 235authenticateDigestNonceCacheCleanup(void *)
2d70df72 236{
237 /*
b20ce974 238 * We walk the hash by noncehex as that is the unique key we
74830fc8 239 * use. For big hash tables we could consider stepping through
240 * the cache, 100/200 entries at a time. Lets see how it flies
241 * first.
2d70df72 242 */
243 digest_nonce_h *nonce;
a4c3b397
AJ
244 debugs(29, 3, "Cleaning the nonce cache now");
245 debugs(29, 3, "Current time: " << current_time.tv_sec);
2d70df72 246 hash_first(digest_nonce_cache);
62e76326 247
2d70df72 248 while ((nonce = ((digest_nonce_h *) hash_next(digest_nonce_cache)))) {
a4c3b397
AJ
249 debugs(29, 3, "nonce entry : " << nonce << " '" << (char *) nonce->key << "'");
250 debugs(29, 4, "Creation time: " << nonce->noncedata.creationtime);
62e76326 251
252 if (authDigestNonceIsStale(nonce)) {
a4c3b397 253 debugs(29, 4, "Removing nonce " << (char *) nonce->key << " from cache due to timeout.");
62e76326 254 assert(nonce->flags.incache);
255 /* invalidate nonce so future requests fail */
3dd52a0b 256 nonce->flags.valid = false;
62e76326 257 /* if it is tied to a auth_user, remove the tie */
258 authDigestNonceUserUnlink(nonce);
259 authDigestNoncePurge(nonce);
260 }
2d70df72 261 }
62e76326 262
a4c3b397 263 debugs(29, 3, "Finished cleaning the nonce cache.");
62e76326 264
dc79fed8 265 if (static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->active())
aee3523a 266 eventAdd("Digest nonce cache maintenance", authenticateDigestNonceCacheCleanup, nullptr, static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->nonceGCInterval, 1);
2d70df72 267}
268
c78aa667 269static void
2d70df72 270authDigestNonceLink(digest_nonce_h * nonce)
271{
aee3523a 272 assert(nonce != nullptr);
742a021b 273 ++nonce->references;
aba0474c 274 assert(nonce->references != 0); // no overflows
a4c3b397 275 debugs(29, 9, "nonce '" << nonce << "' now at '" << nonce->references << "'.");
2d70df72 276}
277
928f3421 278void
2d70df72 279authDigestNonceUnlink(digest_nonce_h * nonce)
280{
aee3523a 281 assert(nonce != nullptr);
62e76326 282
2d70df72 283 if (nonce->references > 0) {
a2f5277a 284 -- nonce->references;
2d70df72 285 } else {
a4c3b397 286 debugs(29, DBG_IMPORTANT, "Attempt to lower nonce " << nonce << " refcount below 0!");
2d70df72 287 }
62e76326 288
a4c3b397 289 debugs(29, 9, "nonce '" << nonce << "' now at '" << nonce->references << "'.");
62e76326 290
2d70df72 291 if (nonce->references == 0)
62e76326 292 authenticateDigestNonceDelete(nonce);
2d70df72 293}
294
928f3421 295const char *
b20ce974 296authenticateDigestNonceNonceHex(const digest_nonce_h * nonce)
2d70df72 297{
298 if (!nonce)
aee3523a 299 return nullptr;
62e76326 300
2f44bd34 301 return (char const *) nonce->key;
2d70df72 302}
303
c78aa667 304static digest_nonce_h *
b20ce974 305authenticateDigestNonceFindNonce(const char *noncehex)
2d70df72 306{
aee3523a 307 digest_nonce_h *nonce = nullptr;
62e76326 308
aee3523a
AR
309 if (noncehex == nullptr)
310 return nullptr;
62e76326 311
b20ce974 312 debugs(29, 9, "looking for noncehex '" << noncehex << "' in the nonce cache.");
62e76326 313
b20ce974 314 nonce = static_cast < digest_nonce_h * >(hash_lookup(digest_nonce_cache, noncehex));
62e76326 315
aee3523a
AR
316 if ((nonce == nullptr) || (strcmp(authenticateDigestNonceNonceHex(nonce), noncehex)))
317 return nullptr;
62e76326 318
a4c3b397 319 debugs(29, 9, "Found nonce '" << nonce << "'");
62e76326 320
2d70df72 321 return nonce;
322}
323
928f3421 324int
2d70df72 325authDigestNonceIsValid(digest_nonce_h * nonce, char nc[9])
326{
d205783b 327 unsigned long intnc;
2d70df72 328 /* do we have a nonce ? */
62e76326 329
2d70df72 330 if (!nonce)
62e76326 331 return 0;
332
aee3523a 333 intnc = strtol(nc, nullptr, 16);
62e76326 334
f5292c64 335 /* has it already been invalidated ? */
336 if (!nonce->flags.valid) {
a4c3b397 337 debugs(29, 4, "Nonce already invalidated");
f5292c64 338 return 0;
339 }
340
341 /* is the nonce-count ok ? */
dc79fed8 342 if (!static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->CheckNonceCount) {
572d2e31
HN
343 /* Ignore client supplied NC */
344 intnc = nonce->nc + 1;
f5292c64 345 }
346
dc79fed8 347 if ((static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->NonceStrictness && intnc != nonce->nc + 1) ||
62e76326 348 intnc < nonce->nc + 1) {
a4c3b397 349 debugs(29, 4, "Nonce count doesn't match");
3dd52a0b 350 nonce->flags.valid = false;
62e76326 351 return 0;
2d70df72 352 }
62e76326 353
d205783b 354 /* increment the nonce count - we've already checked that intnc is a
355 * valid representation for us, so we don't need the test here.
356 */
357 nonce->nc = intnc;
62e76326 358
572d2e31 359 return !authDigestNonceIsStale(nonce);
2d70df72 360}
361
572d2e31 362int
2d70df72 363authDigestNonceIsStale(digest_nonce_h * nonce)
364{
365 /* do we have a nonce ? */
62e76326 366
2d70df72 367 if (!nonce)
62e76326 368 return -1;
369
572d2e31
HN
370 /* Is it already invalidated? */
371 if (!nonce->flags.valid)
372 return -1;
373
2d70df72 374 /* has it's max duration expired? */
dc79fed8 375 if (nonce->noncedata.creationtime + static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->noncemaxduration < current_time.tv_sec) {
a4c3b397 376 debugs(29, 4, "Nonce is too old. " <<
4a7a3d56 377 nonce->noncedata.creationtime << " " <<
dc79fed8 378 static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->noncemaxduration << " " <<
4a7a3d56 379 current_time.tv_sec);
bf8fe701 380
3dd52a0b 381 nonce->flags.valid = false;
62e76326 382 return -1;
2d70df72 383 }
62e76326 384
2d70df72 385 if (nonce->nc > 99999998) {
a4c3b397 386 debugs(29, 4, "Nonce count overflow");
3dd52a0b 387 nonce->flags.valid = false;
62e76326 388 return -1;
2d70df72 389 }
62e76326 390
dc79fed8 391 if (nonce->nc > static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->noncemaxuses) {
a4c3b397 392 debugs(29, 4, "Nonce count over user limit");
3dd52a0b 393 nonce->flags.valid = false;
62e76326 394 return -1;
2d70df72 395 }
62e76326 396
2d70df72 397 /* seems ok */
398 return 0;
399}
400
928f3421
AJ
401/**
402 * \retval 0 the digest is not stale yet
403 * \retval -1 the digest will be stale on the next request
404 */
1dc746da 405int
2d70df72 406authDigestNonceLastRequest(digest_nonce_h * nonce)
407{
408 if (!nonce)
62e76326 409 return -1;
410
2d70df72 411 if (nonce->nc == 99999997) {
a4c3b397 412 debugs(29, 4, "Nonce count about to overflow");
62e76326 413 return -1;
2d70df72 414 }
62e76326 415
dc79fed8 416 if (nonce->nc >= static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest"))->noncemaxuses - 1) {
a4c3b397 417 debugs(29, 4, "Nonce count about to hit user limit");
62e76326 418 return -1;
2d70df72 419 }
62e76326 420
2d70df72 421 /* and other tests are possible. */
422 return 0;
423}
424
aa110616 425void
2d70df72 426authDigestNoncePurge(digest_nonce_h * nonce)
427{
428 if (!nonce)
62e76326 429 return;
430
2d70df72 431 if (!nonce->flags.incache)
62e76326 432 return;
433
4a8b20e8 434 hash_remove_link(digest_nonce_cache, nonce);
62e76326 435
3dd52a0b 436 nonce->flags.incache = false;
62e76326 437
2d70df72 438 /* the cache's link */
439 authDigestNonceUnlink(nonce);
440}
441
0bcb6908 442void
372fccd6 443Auth::Digest::Config::rotateHelpers()
0bcb6908
AJ
444{
445 /* schedule closure of existing helpers */
446 if (digestauthenticators) {
447 helperShutdown(digestauthenticators);
448 }
449
450 /* NP: dynamic helper restart will ensure they start up again as needed. */
451}
452
3616c90c 453bool
dc79fed8 454Auth::Digest::Config::dump(StoreEntry * entry, const char *name, Auth::SchemeConfig * scheme) const
2d70df72 455{
dc79fed8 456 if (!Auth::SchemeConfig::dump(entry, name, scheme))
3616c90c 457 return false;
62e76326 458
3616c90c 459 storeAppendPrintf(entry, "%s %s nonce_max_count %d\n%s %s nonce_max_duration %d seconds\n%s %s nonce_garbage_interval %d seconds\n",
f5691f9c 460 name, "digest", noncemaxuses,
461 name, "digest", (int) noncemaxduration,
462 name, "digest", (int) nonceGCInterval);
3616c90c 463 return true;
2d70df72 464}
465
f5691f9c 466bool
372fccd6 467Auth::Digest::Config::active() const
2d70df72 468{
f5691f9c 469 return authdigest_initialised == 1;
2d70df72 470}
62e76326 471
f5691f9c 472bool
372fccd6 473Auth::Digest::Config::configured() const
2d70df72 474{
aee3523a 475 if ((authenticateProgram != nullptr) &&
48d54e4d 476 (authenticateChildren.n_max != 0) &&
ec980001 477 !realm.isEmpty() && (noncemaxduration > -1))
f5691f9c 478 return true;
62e76326 479
f5691f9c 480 return false;
2d70df72 481}
482
2d70df72 483/* add the [www-|Proxy-]authenticate header on a 407 or 401 reply */
484void
789217a2 485Auth::Digest::Config::fixHeader(Auth::UserRequest::Pointer auth_user_request, HttpReply *rep, Http::HdrType hdrType, HttpRequest *)
2d70df72 486{
58ee2093 487 if (!authenticateProgram)
82b045dc 488 return;
489
572d2e31 490 bool stale = false;
aee3523a 491 digest_nonce_h *nonce = nullptr;
62e76326 492
572d2e31 493 /* on a 407 or 401 we always use a new nonce */
aee3523a 494 if (auth_user_request != nullptr) {
572d2e31 495 Auth::Digest::User *digest_user = dynamic_cast<Auth::Digest::User *>(auth_user_request->user().getRaw());
62e76326 496
572d2e31
HN
497 if (digest_user) {
498 stale = digest_user->credentials() == Auth::Handshake;
499 if (stale) {
500 nonce = digest_user->currentNonce();
501 }
502 }
503 }
504 if (!nonce) {
505 nonce = authenticateDigestNonceNew();
2d70df72 506 }
82b045dc 507
a4c3b397 508 debugs(29, 9, "Sending type:" << hdrType <<
ec980001 509 " header: 'Digest realm=\"" << realm << "\", nonce=\"" <<
b20ce974 510 authenticateDigestNonceNonceHex(nonce) << "\", qop=\"" << QOP_AUTH <<
bf8fe701 511 "\", stale=" << (stale ? "true" : "false"));
82b045dc 512
513 /* in the future, for WWW auth we may want to support the domain entry */
ec980001 514 httpHeaderPutStrf(&rep->header, hdrType, "Digest realm=\"" SQUIDSBUFPH "\", nonce=\"%s\", qop=\"%s\", stale=%s",
b20ce974 515 SQUIDSBUFPRINT(realm), authenticateDigestNonceNonceHex(nonce), QOP_AUTH, stale ? "true" : "false");
2d70df72 516}
517
2d70df72 518/* Initialize helpers and the like for this auth scheme. Called AFTER parsing the
519 * config file */
f5691f9c 520void
dc79fed8 521Auth::Digest::Config::init(Auth::SchemeConfig *)
2d70df72 522{
58ee2093 523 if (authenticateProgram) {
62e76326 524 authenticateDigestNonceSetup();
525 authdigest_initialised = 1;
526
aee3523a 527 if (digestauthenticators == nullptr)
3bd118d6 528 digestauthenticators = helper::Make("digestauthenticator");
62e76326 529
58ee2093 530 digestauthenticators->cmdline = authenticateProgram;
62e76326 531
1af735c7 532 digestauthenticators->childs.updateLimits(authenticateChildren);
62e76326 533
534 digestauthenticators->ipc_type = IPC_STREAM;
535
536 helperOpenServers(digestauthenticators);
2d70df72 537 }
538}
539
62ee09ca 540void
372fccd6 541Auth::Digest::Config::registerWithCacheManager(void)
62ee09ca 542{
8822ebee 543 Mgr::RegisterAction("digestauthenticator",
d9fc6862
A
544 "Digest User Authenticator Stats",
545 authenticateDigestStats, 0, 1);
62ee09ca 546}
2d70df72 547
548/* free any allocated configuration details */
549void
372fccd6 550Auth::Digest::Config::done()
2d70df72 551{
dc79fed8 552 Auth::SchemeConfig::done();
d4806c91 553
d6374be6
AJ
554 authdigest_initialised = 0;
555
556 if (digestauthenticators)
557 helperShutdown(digestauthenticators);
558
d6374be6
AJ
559 if (!shutting_down)
560 return;
561
aee3523a 562 digestauthenticators = nullptr;
d6374be6 563
58ee2093
AJ
564 if (authenticateProgram)
565 wordlistDestroy(&authenticateProgram);
f5691f9c 566}
62e76326 567
d13b829b 568Auth::Digest::Config::Config() :
f53969cc
SM
569 nonceGCInterval(5*60),
570 noncemaxduration(30*60),
571 noncemaxuses(50),
572 NonceStrictness(0),
573 CheckNonceCount(1),
b2b09838 574 PostWorkaround(0)
d13b829b 575{}
2d70df72 576
f5691f9c 577void
dc79fed8 578Auth::Digest::Config::parse(Auth::SchemeConfig * scheme, int n_configured, char *param_str)
2d70df72 579{
97838141 580 if (strcmp(param_str, "nonce_garbage_interval") == 0) {
f5691f9c 581 parse_time_t(&nonceGCInterval);
a37d6070 582 } else if (strcmp(param_str, "nonce_max_duration") == 0) {
f5691f9c 583 parse_time_t(&noncemaxduration);
a37d6070 584 } else if (strcmp(param_str, "nonce_max_count") == 0) {
f5691f9c 585 parse_int((int *) &noncemaxuses);
a37d6070 586 } else if (strcmp(param_str, "nonce_strictness") == 0) {
f5691f9c 587 parse_onoff(&NonceStrictness);
a37d6070 588 } else if (strcmp(param_str, "check_nonce_count") == 0) {
f5691f9c 589 parse_onoff(&CheckNonceCount);
a37d6070 590 } else if (strcmp(param_str, "post_workaround") == 0) {
f5691f9c 591 parse_onoff(&PostWorkaround);
d4806c91 592 } else
dc79fed8 593 Auth::SchemeConfig::parse(scheme, n_configured, param_str);
2d70df72 594}
595
f5691f9c 596const char *
372fccd6 597Auth::Digest::Config::type() const
f5691f9c 598{
d6374be6 599 return Auth::Digest::Scheme::GetInstance()->type();
f5691f9c 600}
601
2d70df72 602static void
603authenticateDigestStats(StoreEntry * sentry)
604{
bf3e8d5a
AJ
605 if (digestauthenticators)
606 digestauthenticators->packStatsInto(sentry, "Digest Authenticator Statistics");
2d70df72 607}
608
609/* NonceUserUnlink: remove the reference to auth_user and unlink the node from the list */
610
c78aa667 611static void
2d70df72 612authDigestNonceUserUnlink(digest_nonce_h * nonce)
613{
aa110616 614 Auth::Digest::User *digest_user;
2d70df72 615 dlink_node *link, *tmplink;
62e76326 616
2d70df72 617 if (!nonce)
62e76326 618 return;
619
f5691f9c 620 if (!nonce->user)
62e76326 621 return;
622
f5691f9c 623 digest_user = nonce->user;
62e76326 624
625 /* unlink from the user list. Yes we're crossing structures but this is the only
2d70df72 626 * time this code is needed
627 */
628 link = digest_user->nonces.head;
62e76326 629
2d70df72 630 while (link) {
62e76326 631 tmplink = link;
632 link = link->next;
633
634 if (tmplink->data == nonce) {
635 dlinkDelete(tmplink, &digest_user->nonces);
636 authDigestNonceUnlink(static_cast < digest_nonce_h * >(tmplink->data));
195b97bf 637 delete tmplink;
aee3523a 638 link = nullptr;
62e76326 639 }
2d70df72 640 }
62e76326 641
f5691f9c 642 /* this reference to user was not locked because freeeing the user frees
26ac0430 643 * the nonce too.
2d70df72 644 */
aee3523a 645 nonce->user = nullptr;
2d70df72 646}
647
572d2e31
HN
648/* authDigesteserLinkNonce: add a nonce to a given user's struct */
649void
aa110616 650authDigestUserLinkNonce(Auth::Digest::User * user, digest_nonce_h * nonce)
2d70df72 651{
652 dlink_node *node;
62e76326 653
d8d76b36 654 if (!user || !nonce || !nonce->user)
62e76326 655 return;
656
aa110616 657 Auth::Digest::User *digest_user = user;
62e76326 658
2d70df72 659 node = digest_user->nonces.head;
62e76326 660
2d70df72 661 while (node && (node->data != nonce))
62e76326 662 node = node->next;
663
2d70df72 664 if (node)
62e76326 665 return;
666
195b97bf 667 node = new dlink_node;
62e76326 668
2d70df72 669 dlinkAddTail(nonce, node, &digest_user->nonces);
62e76326 670
2d70df72 671 authDigestNonceLink(nonce);
62e76326 672
2d70df72 673 /* ping this nonce to this auth user */
aee3523a 674 assert((nonce->user == nullptr) || (nonce->user == user));
62e76326 675
f5691f9c 676 /* we don't lock this reference because removing the user removes the
2d70df72 677 * hash too. Of course if that changes we're stuffed so read the code huh?
678 */
f5691f9c 679 nonce->user = user;
2d70df72 680}
681
682/* setup the necessary info to log the username */
c7baff40 683static Auth::UserRequest::Pointer
d4806c91 684authDigestLogUsername(char *username, Auth::UserRequest::Pointer auth_user_request, const char *requestRealm)
2d70df72 685{
aee3523a 686 assert(auth_user_request != nullptr);
2d70df72 687
688 /* log the username */
1032a194 689 debugs(29, 9, "Creating new user for logging '" << (username?username:"[no username]") << "'");
dc79fed8 690 Auth::User::Pointer digest_user = new Auth::Digest::User(static_cast<Auth::Digest::Config*>(Auth::SchemeConfig::Find("digest")), requestRealm);
2d70df72 691 /* save the credentials */
f5691f9c 692 digest_user->username(username);
2d70df72 693 /* set the auth_user type */
616cfc4c 694 digest_user->auth_type = Auth::AUTH_BROKEN;
2d70df72 695 /* link the request to the user */
f5691f9c 696 auth_user_request->user(digest_user);
f5691f9c 697 return auth_user_request;
2d70df72 698}
699
700/*
701 * Decode a Digest [Proxy-]Auth string, placing the results in the passed
702 * Auth_user structure.
703 */
c7baff40 704Auth::UserRequest::Pointer
7e851a3e 705Auth::Digest::Config::decode(char const *proxy_auth, const HttpRequest *request, const char *aRequestRealm)
2d70df72 706{
2d70df72 707 const char *item;
708 const char *p;
aee3523a
AR
709 const char *pos = nullptr;
710 char *username = nullptr;
2d70df72 711 digest_nonce_h *nonce;
712 int ilen;
2d70df72 713
a4c3b397 714 debugs(29, 9, "beginning");
2d70df72 715
c7baff40 716 Auth::Digest::UserRequest *digest_request = new Auth::Digest::UserRequest();
2d70df72 717
718 /* trim DIGEST from string */
62e76326 719
ba53f4b8 720 while (xisgraph(*proxy_auth))
742a021b 721 ++proxy_auth;
2d70df72 722
723 /* Trim leading whitespace before decoding */
724 while (xisspace(*proxy_auth))
742a021b 725 ++proxy_auth;
2d70df72 726
30abd221 727 String temp(proxy_auth);
62e76326 728
2d70df72 729 while (strListGetItem(&temp, ',', &item, &ilen, &pos)) {
df604ac0 730 /* isolate directive name & value */
6d97f5f1 731 size_t nlen;
a0133f10 732 size_t vlen;
6d97f5f1 733 if ((p = (const char *)memchr(item, '=', ilen)) && (p - item < ilen)) {
f207fe64
FC
734 nlen = p - item;
735 ++p;
a0133f10 736 vlen = ilen - (p - item);
df604ac0 737 } else {
6d97f5f1 738 nlen = ilen;
a0133f10
A
739 vlen = 0;
740 }
9abd1514 741
f2853dd9 742 SBuf keyName(item, nlen);
df604ac0 743 String value;
6a90c2d1 744
a0133f10 745 if (vlen > 0) {
6a90c2d1
AJ
746 // see RFC 2617 section 3.2.1 and 3.2.2 for details on the BNF
747
f2853dd9 748 if (keyName == SBuf("domain",6) || keyName == SBuf("uri",3)) {
6a90c2d1
AJ
749 // domain is Special. Not a quoted-string, must not be de-quoted. But is wrapped in '"'
750 // BUG 3077: uri= can also be sent to us in a mangled (invalid!) form like domain
fb73497a 751 if (vlen > 1 && *p == '"' && *(p + vlen -1) == '"') {
2fe0439c 752 value.assign(p+1, vlen-2);
6a90c2d1 753 }
f2853dd9 754 } else if (keyName == SBuf("qop",3)) {
6a90c2d1
AJ
755 // qop is more special.
756 // On request this must not be quoted-string de-quoted. But is several values wrapped in '"'
757 // On response this is a single un-quoted token.
fb73497a 758 if (vlen > 1 && *p == '"' && *(p + vlen -1) == '"') {
2fe0439c 759 value.assign(p+1, vlen-2);
6a90c2d1 760 } else {
2fe0439c 761 value.assign(p, vlen);
6a90c2d1
AJ
762 }
763 } else if (*p == '"') {
34460e19 764 if (!httpHeaderParseQuotedString(p, vlen, &value)) {
a4c3b397 765 debugs(29, 9, "Failed to parse attribute '" << item << "' in '" << temp << "'");
a0133f10
A
766 continue;
767 }
768 } else {
2fe0439c 769 value.assign(p, vlen);
a0133f10
A
770 }
771 } else {
a4c3b397 772 debugs(29, 9, "Failed to parse attribute '" << item << "' in '" << temp << "'");
6d97f5f1
A
773 continue;
774 }
9abd1514 775
6d97f5f1 776 /* find type */
f5ef79c0 777 const http_digest_attr_type t = DigestFieldsLookupTable.lookup(keyName);
9abd1514 778
9dca980d 779 switch (t) {
6d97f5f1 780 case DIGEST_USERNAME:
bbe0ed86 781 safe_free(username);
7e851a3e
SK
782 if (value.size() != 0) {
783 const auto v = value.termedBuf();
784 if (utf8 && !isValidUtf8String(v, v + value.size())) {
785 auto str = isCP1251EncodingAllowed(request) ? Cp1251ToUtf8(v) : Latin1ToUtf8(v);
786 value = SBufToString(str);
787 }
98b0d0a4 788 username = xstrndup(value.rawBuf(), value.size() + 1);
7e851a3e 789 }
a4c3b397 790 debugs(29, 9, "Found Username '" << username << "'");
6d97f5f1 791 break;
62e76326 792
6d97f5f1 793 case DIGEST_REALM:
bbe0ed86 794 safe_free(digest_request->realm);
98b0d0a4
FB
795 if (value.size() != 0)
796 digest_request->realm = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 797 debugs(29, 9, "Found realm '" << digest_request->realm << "'");
6d97f5f1 798 break;
62e76326 799
6d97f5f1 800 case DIGEST_QOP:
bbe0ed86 801 safe_free(digest_request->qop);
98b0d0a4
FB
802 if (value.size() != 0)
803 digest_request->qop = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 804 debugs(29, 9, "Found qop '" << digest_request->qop << "'");
6d97f5f1 805 break;
62e76326 806
6d97f5f1 807 case DIGEST_ALGORITHM:
bbe0ed86 808 safe_free(digest_request->algorithm);
98b0d0a4
FB
809 if (value.size() != 0)
810 digest_request->algorithm = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 811 debugs(29, 9, "Found algorithm '" << digest_request->algorithm << "'");
6d97f5f1 812 break;
62e76326 813
6d97f5f1 814 case DIGEST_URI:
bbe0ed86 815 safe_free(digest_request->uri);
98b0d0a4
FB
816 if (value.size() != 0)
817 digest_request->uri = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 818 debugs(29, 9, "Found uri '" << digest_request->uri << "'");
6d97f5f1 819 break;
62e76326 820
6d97f5f1 821 case DIGEST_NONCE:
b20ce974 822 safe_free(digest_request->noncehex);
98b0d0a4 823 if (value.size() != 0)
b20ce974 824 digest_request->noncehex = xstrndup(value.rawBuf(), value.size() + 1);
825 debugs(29, 9, "Found nonce '" << digest_request->noncehex << "'");
6d97f5f1 826 break;
62e76326 827
6d97f5f1
A
828 case DIGEST_NC:
829 if (value.size() != 8) {
a4c3b397 830 debugs(29, 9, "Invalid nc '" << value << "' in '" << temp << "'");
6d97f5f1 831 }
9abd1514 832 xstrncpy(digest_request->nc, value.rawBuf(), value.size() + 1);
a4c3b397 833 debugs(29, 9, "Found noncecount '" << digest_request->nc << "'");
6d97f5f1 834 break;
62e76326 835
6d97f5f1 836 case DIGEST_CNONCE:
bbe0ed86 837 safe_free(digest_request->cnonce);
98b0d0a4
FB
838 if (value.size() != 0)
839 digest_request->cnonce = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 840 debugs(29, 9, "Found cnonce '" << digest_request->cnonce << "'");
6d97f5f1 841 break;
62e76326 842
6d97f5f1 843 case DIGEST_RESPONSE:
bbe0ed86 844 safe_free(digest_request->response);
98b0d0a4
FB
845 if (value.size() != 0)
846 digest_request->response = xstrndup(value.rawBuf(), value.size() + 1);
a4c3b397 847 debugs(29, 9, "Found response '" << digest_request->response << "'");
6d97f5f1 848 break;
9abd1514 849
6d97f5f1 850 default:
a4c3b397 851 debugs(29, 3, "Unknown attribute '" << item << "' in '" << temp << "'");
0e134176 852 break;
62e76326 853 }
2d70df72 854 }
62e76326 855
30abd221 856 temp.clean();
2d70df72 857
2d70df72 858 /* now we validate the data given to us */
859
74830fc8 860 /*
861 * TODO: on invalid parameters we should return 400, not 407.
862 * Find some clean way of doing this. perhaps return a valid
863 * struct, and set the direction to clientwards combined with
864 * a change to the clientwards handling code (ie let the
865 * clientwards call set the error type (but limited to known
866 * correct values - 400/401/407
867 */
2d70df72 868
59a98343 869 /* 2069 requirements */
62e76326 870
1aca58c0
FC
871 // return value.
872 Auth::UserRequest::Pointer rv;
59a98343
HN
873 /* do we have a username ? */
874 if (!username || username[0] == '\0') {
1aca58c0 875 debugs(29, 2, "Empty or not present username");
d4806c91 876 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
877 safe_free(username);
878 return rv;
2d70df72 879 }
62e76326 880
920d1c9d
HN
881 /* Sanity check of the username.
882 * " can not be allowed in usernames until * the digest helper protocol
883 * have been redone
884 */
885 if (strchr(username, '"')) {
1aca58c0 886 debugs(29, 2, "Unacceptable username '" << username << "'");
d4806c91 887 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
888 safe_free(username);
889 return rv;
2d70df72 890 }
62e76326 891
59a98343
HN
892 /* do we have a realm ? */
893 if (!digest_request->realm || digest_request->realm[0] == '\0') {
1aca58c0 894 debugs(29, 2, "Empty or not present realm");
d4806c91 895 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
896 safe_free(username);
897 return rv;
2d70df72 898 }
62e76326 899
59a98343 900 /* and a nonce? */
b20ce974 901 if (!digest_request->noncehex || digest_request->noncehex[0] == '\0') {
1aca58c0 902 debugs(29, 2, "Empty or not present nonce");
d4806c91 903 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
904 safe_free(username);
905 return rv;
2d70df72 906 }
62e76326 907
74830fc8 908 /* we can't check the URI just yet. We'll check it in the
6649f955 909 * authenticate phase, but needs to be given */
59a98343 910 if (!digest_request->uri || digest_request->uri[0] == '\0') {
1aca58c0 911 debugs(29, 2, "Missing URI field");
d4806c91 912 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
913 safe_free(username);
914 return rv;
6649f955 915 }
2d70df72 916
917 /* is the response the correct length? */
2d70df72 918 if (!digest_request->response || strlen(digest_request->response) != 32) {
1aca58c0 919 debugs(29, 2, "Response length invalid");
d4806c91 920 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
921 safe_free(username);
922 return rv;
2d70df72 923 }
62e76326 924
59a98343
HN
925 /* check the algorithm is present and supported */
926 if (!digest_request->algorithm)
927 digest_request->algorithm = xstrndup("MD5", 4);
928 else if (strcmp(digest_request->algorithm, "MD5")
929 && strcmp(digest_request->algorithm, "MD5-sess")) {
1aca58c0 930 debugs(29, 2, "invalid algorithm specified!");
d4806c91 931 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
932 safe_free(username);
933 return rv;
2d70df72 934 }
62e76326 935
59a98343
HN
936 /* 2617 requirements, indicated by qop */
937 if (digest_request->qop) {
938
6d97f5f1
A
939 /* check the qop is what we expected. */
940 if (strcmp(digest_request->qop, QOP_AUTH) != 0) {
941 /* we received a qop option we didn't send */
1aca58c0 942 debugs(29, 2, "Invalid qop option received");
d4806c91 943 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
944 safe_free(username);
945 return rv;
6d97f5f1
A
946 }
947
948 /* check cnonce */
949 if (!digest_request->cnonce || digest_request->cnonce[0] == '\0') {
1aca58c0 950 debugs(29, 2, "Missing cnonce field");
d4806c91 951 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
952 safe_free(username);
953 return rv;
6d97f5f1
A
954 }
955
956 /* check nc */
957 if (strlen(digest_request->nc) != 8 || strspn(digest_request->nc, "0123456789abcdefABCDEF") != 8) {
1aca58c0 958 debugs(29, 2, "invalid nonce count");
d4806c91 959 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
960 safe_free(username);
961 return rv;
6d97f5f1 962 }
59a98343 963 } else {
6d97f5f1 964 /* cnonce and nc both require qop */
7830d88a 965 if (digest_request->cnonce || digest_request->nc[0] != '\0') {
1aca58c0 966 debugs(29, 2, "missing qop!");
d4806c91 967 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
968 safe_free(username);
969 return rv;
6d97f5f1 970 }
2d70df72 971 }
62e76326 972
59a98343
HN
973 /** below nonce state dependent **/
974
975 /* now the nonce */
b20ce974 976 nonce = authenticateDigestNonceFindNonce(digest_request->noncehex);
572d2e31
HN
977 /* check that we're not being hacked / the username hasn't changed */
978 if (nonce && nonce->user && strcmp(username, nonce->user->username())) {
979 debugs(29, 2, "Username for the nonce does not equal the username for the request");
aee3523a 980 nonce = nullptr;
572d2e31 981 }
6b634dc3 982
59a98343
HN
983 if (!nonce) {
984 /* we couldn't find a matching nonce! */
572d2e31
HN
985 debugs(29, 2, "Unexpected or invalid nonce received from " << username);
986 Auth::UserRequest::Pointer auth_request = authDigestLogUsername(username, digest_request, aRequestRealm);
987 auth_request->user()->credentials(Auth::Handshake);
1aca58c0 988 safe_free(username);
572d2e31 989 return auth_request;
2d70df72 990 }
62e76326 991
59a98343
HN
992 digest_request->nonce = nonce;
993 authDigestNonceLink(nonce);
994
995 /* check that we're not being hacked / the username hasn't changed */
996 if (nonce->user && strcmp(username, nonce->user->username())) {
1aca58c0 997 debugs(29, 2, "Username for the nonce does not equal the username for the request");
d4806c91 998 rv = authDigestLogUsername(username, digest_request, aRequestRealm);
1aca58c0
FC
999 safe_free(username);
1000 return rv;
2d70df72 1001 }
62e76326 1002
2d70df72 1003 /* the method we'll check at the authenticate step as well */
1004
2f8abb64 1005 /* we don't send or parse opaques. Ok so we're flexible ... */
2d70df72 1006
1007 /* find the user */
aa110616 1008 Auth::Digest::User *digest_user;
f5691f9c 1009
d87154ee 1010 Auth::User::Pointer auth_user;
2d70df72 1011
d4806c91 1012 SBuf key = Auth::User::BuildUserKey(username, aRequestRealm);
0593bae5 1013 if (key.isEmpty() || !(auth_user = Auth::Digest::User::Cache()->lookup(key))) {
62e76326 1014 /* the user doesn't exist in the username cache yet */
a4c3b397 1015 debugs(29, 9, "Creating new digest user '" << username << "'");
d4806c91 1016 digest_user = new Auth::Digest::User(this, aRequestRealm);
f5691f9c 1017 /* auth_user is a parent */
1018 auth_user = digest_user;
62e76326 1019 /* save the username */
f5691f9c 1020 digest_user->username(username);
62e76326 1021 /* set the user type */
616cfc4c 1022 digest_user->auth_type = Auth::AUTH_DIGEST;
62e76326 1023 /* this auth_user struct is the one to get added to the
1024 * username cache */
1025 /* store user in hash's */
f5691f9c 1026 digest_user->addToNameCache();
df80d445 1027
62e76326 1028 /*
1029 * Add the digest to the user so we can tell if a hacking
1030 * or spoofing attack is taking place. We do this by assuming
1031 * the user agent won't change user name without warning.
1032 */
f5691f9c 1033 authDigestUserLinkNonce(digest_user, nonce);
65cbd5a7
GD
1034
1035 /* auth_user is now linked, we reset these values
1036 * after external auth occurs anyway */
1037 auth_user->expiretime = current_time.tv_sec;
2d70df72 1038 } else {
a4c3b397 1039 debugs(29, 9, "Found user '" << username << "' in the user cache as '" << auth_user << "'");
aa110616 1040 digest_user = static_cast<Auth::Digest::User *>(auth_user.getRaw());
156c3aae 1041 digest_user->credentials(Auth::Unchecked);
62e76326 1042 xfree(username);
2d70df72 1043 }
62e76326 1044
2d70df72 1045 /*link the request and the user */
aee3523a 1046 assert(digest_request != nullptr);
82b045dc 1047
f5691f9c 1048 digest_request->user(digest_user);
a4c3b397 1049 debugs(29, 9, "username = '" << digest_user->username() << "'\nrealm = '" <<
bf8fe701 1050 digest_request->realm << "'\nqop = '" << digest_request->qop <<
1051 "'\nalgorithm = '" << digest_request->algorithm << "'\nuri = '" <<
b20ce974 1052 digest_request->uri << "'\nnonce = '" << digest_request->noncehex <<
bf8fe701 1053 "'\nnc = '" << digest_request->nc << "'\ncnonce = '" <<
1054 digest_request->cnonce << "'\nresponse = '" <<
1055 digest_request->response << "'\ndigestnonce = '" << nonce << "'");
2d70df72 1056
f5691f9c 1057 return digest_request;
2d70df72 1058}
f53969cc 1059