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