digest_sspi.c (21191B)
1 /*************************************************************************** 2 * _ _ ____ _ 3 * Project ___| | | | _ \| | 4 * / __| | | | |_) | | 5 * | (__| |_| | _ <| |___ 6 * \___|\___/|_| \_\_____| 7 * 8 * Copyright (C) Steve Holme, <steve_holme@hotmail.com>. 9 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. 10 * 11 * This software is licensed as described in the file COPYING, which 12 * you should have received as part of this distribution. The terms 13 * are also available at https://curl.se/docs/copyright.html. 14 * 15 * You may opt to use, copy, modify, merge, publish, distribute and/or sell 16 * copies of the Software, and permit persons to whom the Software is 17 * furnished to do so, under the terms of the COPYING file. 18 * 19 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 20 * KIND, either express or implied. 21 * 22 * SPDX-License-Identifier: curl 23 * 24 * RFC2831 DIGEST-MD5 authentication 25 * 26 ***************************************************************************/ 27 28 #include "../curl_setup.h" 29 30 #if defined(USE_WINDOWS_SSPI) && !defined(CURL_DISABLE_DIGEST_AUTH) 31 32 #include <curl/curl.h> 33 34 #include "vauth.h" 35 #include "digest.h" 36 #include "../urldata.h" 37 #include "../curlx/warnless.h" 38 #include "../curlx/multibyte.h" 39 #include "../sendf.h" 40 #include "../strdup.h" 41 #include "../strcase.h" 42 #include "../strerror.h" 43 44 /* The last #include files should be: */ 45 #include "../curl_memory.h" 46 #include "../memdebug.h" 47 48 /* 49 * Curl_auth_is_digest_supported() 50 * 51 * This is used to evaluate if DIGEST is supported. 52 * 53 * Parameters: None 54 * 55 * Returns TRUE if DIGEST is supported by Windows SSPI. 56 */ 57 bool Curl_auth_is_digest_supported(void) 58 { 59 PSecPkgInfo SecurityPackage; 60 SECURITY_STATUS status; 61 62 /* Query the security package for Digest */ 63 status = 64 Curl_pSecFn->QuerySecurityPackageInfo( 65 (TCHAR *)CURL_UNCONST(TEXT(SP_NAME_DIGEST)), 66 &SecurityPackage); 67 68 /* Release the package buffer as it is not required anymore */ 69 if(status == SEC_E_OK) { 70 Curl_pSecFn->FreeContextBuffer(SecurityPackage); 71 } 72 73 return status == SEC_E_OK; 74 } 75 76 /* 77 * Curl_auth_create_digest_md5_message() 78 * 79 * This is used to generate an already encoded DIGEST-MD5 response message 80 * ready for sending to the recipient. 81 * 82 * Parameters: 83 * 84 * data [in] - The session handle. 85 * chlg [in] - The challenge message. 86 * userp [in] - The username in the format User or Domain\User. 87 * passwdp [in] - The user's password. 88 * service [in] - The service type such as http, smtp, pop or imap. 89 * out [out] - The result storage. 90 * 91 * Returns CURLE_OK on success. 92 */ 93 CURLcode Curl_auth_create_digest_md5_message(struct Curl_easy *data, 94 const struct bufref *chlg, 95 const char *userp, 96 const char *passwdp, 97 const char *service, 98 struct bufref *out) 99 { 100 CURLcode result = CURLE_OK; 101 TCHAR *spn = NULL; 102 size_t token_max = 0; 103 unsigned char *output_token = NULL; 104 CredHandle credentials; 105 CtxtHandle context; 106 PSecPkgInfo SecurityPackage; 107 SEC_WINNT_AUTH_IDENTITY identity; 108 SEC_WINNT_AUTH_IDENTITY *p_identity; 109 SecBuffer chlg_buf; 110 SecBuffer resp_buf; 111 SecBufferDesc chlg_desc; 112 SecBufferDesc resp_desc; 113 SECURITY_STATUS status; 114 unsigned long attrs; 115 TimeStamp expiry; /* For Windows 9x compatibility of SSPI calls */ 116 117 /* Ensure we have a valid challenge message */ 118 if(!Curl_bufref_len(chlg)) { 119 infof(data, "DIGEST-MD5 handshake failure (empty challenge message)"); 120 return CURLE_BAD_CONTENT_ENCODING; 121 } 122 123 /* Query the security package for DigestSSP */ 124 status = 125 Curl_pSecFn->QuerySecurityPackageInfo( 126 (TCHAR *)CURL_UNCONST(TEXT(SP_NAME_DIGEST)), 127 &SecurityPackage); 128 if(status != SEC_E_OK) { 129 failf(data, "SSPI: could not get auth info"); 130 return CURLE_AUTH_ERROR; 131 } 132 133 token_max = SecurityPackage->cbMaxToken; 134 135 /* Release the package buffer as it is not required anymore */ 136 Curl_pSecFn->FreeContextBuffer(SecurityPackage); 137 138 /* Allocate our response buffer */ 139 output_token = malloc(token_max); 140 if(!output_token) 141 return CURLE_OUT_OF_MEMORY; 142 143 /* Generate our SPN */ 144 spn = Curl_auth_build_spn(service, data->conn->host.name, NULL); 145 if(!spn) { 146 free(output_token); 147 return CURLE_OUT_OF_MEMORY; 148 } 149 150 if(userp && *userp) { 151 /* Populate our identity structure */ 152 result = Curl_create_sspi_identity(userp, passwdp, &identity); 153 if(result) { 154 free(spn); 155 free(output_token); 156 return result; 157 } 158 159 /* Allow proper cleanup of the identity structure */ 160 p_identity = &identity; 161 } 162 else 163 /* Use the current Windows user */ 164 p_identity = NULL; 165 166 /* Acquire our credentials handle */ 167 status = Curl_pSecFn->AcquireCredentialsHandle(NULL, 168 (TCHAR *)CURL_UNCONST(TEXT(SP_NAME_DIGEST)), 169 SECPKG_CRED_OUTBOUND, NULL, 170 p_identity, NULL, NULL, 171 &credentials, &expiry); 172 173 if(status != SEC_E_OK) { 174 Curl_sspi_free_identity(p_identity); 175 free(spn); 176 free(output_token); 177 return CURLE_LOGIN_DENIED; 178 } 179 180 /* Setup the challenge "input" security buffer */ 181 chlg_desc.ulVersion = SECBUFFER_VERSION; 182 chlg_desc.cBuffers = 1; 183 chlg_desc.pBuffers = &chlg_buf; 184 chlg_buf.BufferType = SECBUFFER_TOKEN; 185 chlg_buf.pvBuffer = CURL_UNCONST(Curl_bufref_ptr(chlg)); 186 chlg_buf.cbBuffer = curlx_uztoul(Curl_bufref_len(chlg)); 187 188 /* Setup the response "output" security buffer */ 189 resp_desc.ulVersion = SECBUFFER_VERSION; 190 resp_desc.cBuffers = 1; 191 resp_desc.pBuffers = &resp_buf; 192 resp_buf.BufferType = SECBUFFER_TOKEN; 193 resp_buf.pvBuffer = output_token; 194 resp_buf.cbBuffer = curlx_uztoul(token_max); 195 196 /* Generate our response message */ 197 status = Curl_pSecFn->InitializeSecurityContext(&credentials, NULL, spn, 198 0, 0, 0, &chlg_desc, 0, 199 &context, &resp_desc, &attrs, 200 &expiry); 201 202 if(status == SEC_I_COMPLETE_NEEDED || 203 status == SEC_I_COMPLETE_AND_CONTINUE) 204 Curl_pSecFn->CompleteAuthToken(&credentials, &resp_desc); 205 else if(status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) { 206 #if !defined(CURL_DISABLE_VERBOSE_STRINGS) 207 char buffer[STRERROR_LEN]; 208 #endif 209 210 Curl_pSecFn->FreeCredentialsHandle(&credentials); 211 Curl_sspi_free_identity(p_identity); 212 free(spn); 213 free(output_token); 214 215 if(status == SEC_E_INSUFFICIENT_MEMORY) 216 return CURLE_OUT_OF_MEMORY; 217 218 #if !defined(CURL_DISABLE_VERBOSE_STRINGS) 219 infof(data, "schannel: InitializeSecurityContext failed: %s", 220 Curl_sspi_strerror(status, buffer, sizeof(buffer))); 221 #endif 222 223 return CURLE_AUTH_ERROR; 224 } 225 226 /* Return the response. */ 227 Curl_bufref_set(out, output_token, resp_buf.cbBuffer, curl_free); 228 229 /* Free our handles */ 230 Curl_pSecFn->DeleteSecurityContext(&context); 231 Curl_pSecFn->FreeCredentialsHandle(&credentials); 232 233 /* Free the identity structure */ 234 Curl_sspi_free_identity(p_identity); 235 236 /* Free the SPN */ 237 free(spn); 238 239 return result; 240 } 241 242 /* 243 * Curl_override_sspi_http_realm() 244 * 245 * This is used to populate the domain in an SSPI identity structure 246 * The realm is extracted from the challenge message and used as the 247 * domain if it is not already explicitly set. 248 * 249 * Parameters: 250 * 251 * chlg [in] - The challenge message. 252 * identity [in/out] - The identity structure. 253 * 254 * Returns CURLE_OK on success. 255 */ 256 CURLcode Curl_override_sspi_http_realm(const char *chlg, 257 SEC_WINNT_AUTH_IDENTITY *identity) 258 { 259 xcharp_u domain, dup_domain; 260 261 /* If domain is blank or unset, check challenge message for realm */ 262 if(!identity->Domain || !identity->DomainLength) { 263 for(;;) { 264 char value[DIGEST_MAX_VALUE_LENGTH]; 265 char content[DIGEST_MAX_CONTENT_LENGTH]; 266 267 /* Pass all additional spaces here */ 268 while(*chlg && ISBLANK(*chlg)) 269 chlg++; 270 271 /* Extract a value=content pair */ 272 if(Curl_auth_digest_get_pair(chlg, value, content, &chlg)) { 273 if(curl_strequal(value, "realm")) { 274 275 /* Setup identity's domain and length */ 276 domain.tchar_ptr = curlx_convert_UTF8_to_tchar(content); 277 if(!domain.tchar_ptr) 278 return CURLE_OUT_OF_MEMORY; 279 280 dup_domain.tchar_ptr = _tcsdup(domain.tchar_ptr); 281 if(!dup_domain.tchar_ptr) { 282 curlx_unicodefree(domain.tchar_ptr); 283 return CURLE_OUT_OF_MEMORY; 284 } 285 286 free(identity->Domain); 287 identity->Domain = dup_domain.tbyte_ptr; 288 identity->DomainLength = curlx_uztoul(_tcslen(dup_domain.tchar_ptr)); 289 dup_domain.tchar_ptr = NULL; 290 291 curlx_unicodefree(domain.tchar_ptr); 292 } 293 else { 294 /* Unknown specifier, ignore it! */ 295 } 296 } 297 else 298 break; /* We are done here */ 299 300 /* Pass all additional spaces here */ 301 while(*chlg && ISBLANK(*chlg)) 302 chlg++; 303 304 /* Allow the list to be comma-separated */ 305 if(',' == *chlg) 306 chlg++; 307 } 308 } 309 310 return CURLE_OK; 311 } 312 313 /* 314 * Curl_auth_decode_digest_http_message() 315 * 316 * This is used to decode an HTTP DIGEST challenge message into the separate 317 * attributes. 318 * 319 * Parameters: 320 * 321 * chlg [in] - The challenge message. 322 * digest [in/out] - The digest data struct being used and modified. 323 * 324 * Returns CURLE_OK on success. 325 */ 326 CURLcode Curl_auth_decode_digest_http_message(const char *chlg, 327 struct digestdata *digest) 328 { 329 size_t chlglen = strlen(chlg); 330 331 /* We had an input token before so if there is another one now that means we 332 provided bad credentials in the previous request or it is stale. */ 333 if(digest->input_token) { 334 bool stale = FALSE; 335 const char *p = chlg; 336 337 /* Check for the 'stale' directive */ 338 for(;;) { 339 char value[DIGEST_MAX_VALUE_LENGTH]; 340 char content[DIGEST_MAX_CONTENT_LENGTH]; 341 342 while(*p && ISBLANK(*p)) 343 p++; 344 345 if(!Curl_auth_digest_get_pair(p, value, content, &p)) 346 break; 347 348 if(curl_strequal(value, "stale") && 349 curl_strequal(content, "true")) { 350 stale = TRUE; 351 break; 352 } 353 354 while(*p && ISBLANK(*p)) 355 p++; 356 357 if(',' == *p) 358 p++; 359 } 360 361 if(stale) 362 Curl_auth_digest_cleanup(digest); 363 else 364 return CURLE_LOGIN_DENIED; 365 } 366 367 /* Store the challenge for use later */ 368 digest->input_token = (BYTE *) Curl_memdup(chlg, chlglen + 1); 369 if(!digest->input_token) 370 return CURLE_OUT_OF_MEMORY; 371 372 digest->input_token_len = chlglen; 373 374 return CURLE_OK; 375 } 376 377 /* 378 * Curl_auth_create_digest_http_message() 379 * 380 * This is used to generate an HTTP DIGEST response message ready for sending 381 * to the recipient. 382 * 383 * Parameters: 384 * 385 * data [in] - The session handle. 386 * userp [in] - The username in the format User or Domain\User. 387 * passwdp [in] - The user's password. 388 * request [in] - The HTTP request. 389 * uripath [in] - The path of the HTTP uri. 390 * digest [in/out] - The digest data struct being used and modified. 391 * outptr [in/out] - The address where a pointer to newly allocated memory 392 * holding the result will be stored upon completion. 393 * outlen [out] - The length of the output message. 394 * 395 * Returns CURLE_OK on success. 396 */ 397 CURLcode Curl_auth_create_digest_http_message(struct Curl_easy *data, 398 const char *userp, 399 const char *passwdp, 400 const unsigned char *request, 401 const unsigned char *uripath, 402 struct digestdata *digest, 403 char **outptr, size_t *outlen) 404 { 405 size_t token_max; 406 char *resp; 407 BYTE *output_token; 408 size_t output_token_len = 0; 409 PSecPkgInfo SecurityPackage; 410 SecBuffer chlg_buf[5]; 411 SecBufferDesc chlg_desc; 412 SECURITY_STATUS status; 413 414 (void) data; 415 416 /* Query the security package for DigestSSP */ 417 status = 418 Curl_pSecFn->QuerySecurityPackageInfo( 419 (TCHAR *)CURL_UNCONST(TEXT(SP_NAME_DIGEST)), 420 &SecurityPackage); 421 if(status != SEC_E_OK) { 422 failf(data, "SSPI: could not get auth info"); 423 return CURLE_AUTH_ERROR; 424 } 425 426 token_max = SecurityPackage->cbMaxToken; 427 428 /* Release the package buffer as it is not required anymore */ 429 Curl_pSecFn->FreeContextBuffer(SecurityPackage); 430 431 /* Allocate the output buffer according to the max token size as indicated 432 by the security package */ 433 output_token = malloc(token_max); 434 if(!output_token) { 435 return CURLE_OUT_OF_MEMORY; 436 } 437 438 /* If the user/passwd that was used to make the identity for http_context 439 has changed then delete that context. */ 440 if((userp && !digest->user) || (!userp && digest->user) || 441 (passwdp && !digest->passwd) || (!passwdp && digest->passwd) || 442 (userp && digest->user && Curl_timestrcmp(userp, digest->user)) || 443 (passwdp && digest->passwd && Curl_timestrcmp(passwdp, digest->passwd))) { 444 if(digest->http_context) { 445 Curl_pSecFn->DeleteSecurityContext(digest->http_context); 446 Curl_safefree(digest->http_context); 447 } 448 Curl_safefree(digest->user); 449 Curl_safefree(digest->passwd); 450 } 451 452 if(digest->http_context) { 453 chlg_desc.ulVersion = SECBUFFER_VERSION; 454 chlg_desc.cBuffers = 5; 455 chlg_desc.pBuffers = chlg_buf; 456 chlg_buf[0].BufferType = SECBUFFER_TOKEN; 457 chlg_buf[0].pvBuffer = NULL; 458 chlg_buf[0].cbBuffer = 0; 459 chlg_buf[1].BufferType = SECBUFFER_PKG_PARAMS; 460 chlg_buf[1].pvBuffer = CURL_UNCONST(request); 461 chlg_buf[1].cbBuffer = curlx_uztoul(strlen((const char *) request)); 462 chlg_buf[2].BufferType = SECBUFFER_PKG_PARAMS; 463 chlg_buf[2].pvBuffer = CURL_UNCONST(uripath); 464 chlg_buf[2].cbBuffer = curlx_uztoul(strlen((const char *) uripath)); 465 chlg_buf[3].BufferType = SECBUFFER_PKG_PARAMS; 466 chlg_buf[3].pvBuffer = NULL; 467 chlg_buf[3].cbBuffer = 0; 468 chlg_buf[4].BufferType = SECBUFFER_PADDING; 469 chlg_buf[4].pvBuffer = output_token; 470 chlg_buf[4].cbBuffer = curlx_uztoul(token_max); 471 472 status = Curl_pSecFn->MakeSignature(digest->http_context, 0, &chlg_desc, 473 0); 474 if(status == SEC_E_OK) 475 output_token_len = chlg_buf[4].cbBuffer; 476 else { /* delete the context so a new one can be made */ 477 infof(data, "digest_sspi: MakeSignature failed, error 0x%08lx", 478 (long)status); 479 Curl_pSecFn->DeleteSecurityContext(digest->http_context); 480 Curl_safefree(digest->http_context); 481 } 482 } 483 484 if(!digest->http_context) { 485 CredHandle credentials; 486 SEC_WINNT_AUTH_IDENTITY identity; 487 SEC_WINNT_AUTH_IDENTITY *p_identity; 488 SecBuffer resp_buf; 489 SecBufferDesc resp_desc; 490 unsigned long attrs; 491 TimeStamp expiry; /* For Windows 9x compatibility of SSPI calls */ 492 TCHAR *spn; 493 494 /* free the copy of user/passwd used to make the previous identity */ 495 Curl_safefree(digest->user); 496 Curl_safefree(digest->passwd); 497 498 if(userp && *userp) { 499 /* Populate our identity structure */ 500 if(Curl_create_sspi_identity(userp, passwdp, &identity)) { 501 free(output_token); 502 return CURLE_OUT_OF_MEMORY; 503 } 504 505 /* Populate our identity domain */ 506 if(Curl_override_sspi_http_realm((const char *) digest->input_token, 507 &identity)) { 508 free(output_token); 509 return CURLE_OUT_OF_MEMORY; 510 } 511 512 /* Allow proper cleanup of the identity structure */ 513 p_identity = &identity; 514 } 515 else 516 /* Use the current Windows user */ 517 p_identity = NULL; 518 519 if(userp) { 520 digest->user = strdup(userp); 521 522 if(!digest->user) { 523 free(output_token); 524 return CURLE_OUT_OF_MEMORY; 525 } 526 } 527 528 if(passwdp) { 529 digest->passwd = strdup(passwdp); 530 531 if(!digest->passwd) { 532 free(output_token); 533 Curl_safefree(digest->user); 534 return CURLE_OUT_OF_MEMORY; 535 } 536 } 537 538 /* Acquire our credentials handle */ 539 status = Curl_pSecFn->AcquireCredentialsHandle(NULL, 540 (TCHAR *)CURL_UNCONST(TEXT(SP_NAME_DIGEST)), 541 SECPKG_CRED_OUTBOUND, NULL, 542 p_identity, NULL, NULL, 543 &credentials, &expiry); 544 if(status != SEC_E_OK) { 545 Curl_sspi_free_identity(p_identity); 546 free(output_token); 547 548 return CURLE_LOGIN_DENIED; 549 } 550 551 /* Setup the challenge "input" security buffer if present */ 552 chlg_desc.ulVersion = SECBUFFER_VERSION; 553 chlg_desc.cBuffers = 3; 554 chlg_desc.pBuffers = chlg_buf; 555 chlg_buf[0].BufferType = SECBUFFER_TOKEN; 556 chlg_buf[0].pvBuffer = digest->input_token; 557 chlg_buf[0].cbBuffer = curlx_uztoul(digest->input_token_len); 558 chlg_buf[1].BufferType = SECBUFFER_PKG_PARAMS; 559 chlg_buf[1].pvBuffer = CURL_UNCONST(request); 560 chlg_buf[1].cbBuffer = curlx_uztoul(strlen((const char *) request)); 561 chlg_buf[2].BufferType = SECBUFFER_PKG_PARAMS; 562 chlg_buf[2].pvBuffer = NULL; 563 chlg_buf[2].cbBuffer = 0; 564 565 /* Setup the response "output" security buffer */ 566 resp_desc.ulVersion = SECBUFFER_VERSION; 567 resp_desc.cBuffers = 1; 568 resp_desc.pBuffers = &resp_buf; 569 resp_buf.BufferType = SECBUFFER_TOKEN; 570 resp_buf.pvBuffer = output_token; 571 resp_buf.cbBuffer = curlx_uztoul(token_max); 572 573 spn = curlx_convert_UTF8_to_tchar((const char *) uripath); 574 if(!spn) { 575 Curl_pSecFn->FreeCredentialsHandle(&credentials); 576 577 Curl_sspi_free_identity(p_identity); 578 free(output_token); 579 580 return CURLE_OUT_OF_MEMORY; 581 } 582 583 /* Allocate our new context handle */ 584 digest->http_context = calloc(1, sizeof(CtxtHandle)); 585 if(!digest->http_context) { 586 curlx_unicodefree(spn); 587 Curl_sspi_free_identity(p_identity); 588 free(output_token); 589 return CURLE_OUT_OF_MEMORY; 590 } 591 592 /* Generate our response message */ 593 status = Curl_pSecFn->InitializeSecurityContext(&credentials, NULL, 594 spn, 595 ISC_REQ_USE_HTTP_STYLE, 0, 0, 596 &chlg_desc, 0, 597 digest->http_context, 598 &resp_desc, &attrs, &expiry); 599 curlx_unicodefree(spn); 600 601 if(status == SEC_I_COMPLETE_NEEDED || 602 status == SEC_I_COMPLETE_AND_CONTINUE) 603 Curl_pSecFn->CompleteAuthToken(&credentials, &resp_desc); 604 else if(status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED) { 605 #if !defined(CURL_DISABLE_VERBOSE_STRINGS) 606 char buffer[STRERROR_LEN]; 607 #endif 608 609 Curl_pSecFn->FreeCredentialsHandle(&credentials); 610 611 Curl_sspi_free_identity(p_identity); 612 free(output_token); 613 614 Curl_safefree(digest->http_context); 615 616 if(status == SEC_E_INSUFFICIENT_MEMORY) 617 return CURLE_OUT_OF_MEMORY; 618 619 #if !defined(CURL_DISABLE_VERBOSE_STRINGS) 620 infof(data, "schannel: InitializeSecurityContext failed: %s", 621 Curl_sspi_strerror(status, buffer, sizeof(buffer))); 622 #endif 623 624 return CURLE_AUTH_ERROR; 625 } 626 627 output_token_len = resp_buf.cbBuffer; 628 629 Curl_pSecFn->FreeCredentialsHandle(&credentials); 630 Curl_sspi_free_identity(p_identity); 631 } 632 633 resp = malloc(output_token_len + 1); 634 if(!resp) { 635 free(output_token); 636 637 return CURLE_OUT_OF_MEMORY; 638 } 639 640 /* Copy the generated response */ 641 memcpy(resp, output_token, output_token_len); 642 resp[output_token_len] = 0; 643 644 /* Return the response */ 645 *outptr = resp; 646 *outlen = output_token_len; 647 648 /* Free the response buffer */ 649 free(output_token); 650 651 return CURLE_OK; 652 } 653 654 /* 655 * Curl_auth_digest_cleanup() 656 * 657 * This is used to clean up the digest specific data. 658 * 659 * Parameters: 660 * 661 * digest [in/out] - The digest data struct being cleaned up. 662 * 663 */ 664 void Curl_auth_digest_cleanup(struct digestdata *digest) 665 { 666 /* Free the input token */ 667 Curl_safefree(digest->input_token); 668 669 /* Reset any variables */ 670 digest->input_token_len = 0; 671 672 /* Delete security context */ 673 if(digest->http_context) { 674 Curl_pSecFn->DeleteSecurityContext(digest->http_context); 675 Curl_safefree(digest->http_context); 676 } 677 678 /* Free the copy of user/passwd used to make the identity for http_context */ 679 Curl_safefree(digest->user); 680 Curl_safefree(digest->passwd); 681 } 682 683 #endif /* USE_WINDOWS_SSPI && !CURL_DISABLE_DIGEST_AUTH */