1 /***************************************************************************
3 * Project ___| | | | _ \| |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
8 * Copyright (C) 1998 - 2016, Daniel Stenberg, <daniel@haxx.se>, et al.
10 * This software is licensed as described in the file COPYING, which
11 * you should have received as part of this distribution. The terms
12 * are also available at https://curl.haxx.se/docs/copyright.html.
14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15 * copies of the Software, and permit persons to whom the Software is
16 * furnished to do so, under the terms of the COPYING file.
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
21 * RFC2831 DIGEST-MD5 authentication
23 ***************************************************************************/
25 #include "curl_setup.h"
27 #if !defined(CURL_DISABLE_CRYPTO_AUTH)
29 #include <curl/curl.h>
31 #include "vauth/vauth.h"
32 #include "vauth/digest.h"
34 #include "curl_base64.h"
35 #include "curl_hmac.h"
37 #include "vtls/vtls.h"
41 #include "non-ascii.h" /* included for Curl_convert_... prototypes */
42 #include "curl_printf.h"
45 /* The last #include files should be: */
46 #include "curl_memory.h"
49 #if !defined(USE_WINDOWS_SSPI)
50 #define DIGEST_QOP_VALUE_AUTH (1 << 0)
51 #define DIGEST_QOP_VALUE_AUTH_INT (1 << 1)
52 #define DIGEST_QOP_VALUE_AUTH_CONF (1 << 2)
54 #define DIGEST_QOP_VALUE_STRING_AUTH "auth"
55 #define DIGEST_QOP_VALUE_STRING_AUTH_INT "auth-int"
56 #define DIGEST_QOP_VALUE_STRING_AUTH_CONF "auth-conf"
58 /* The CURL_OUTPUT_DIGEST_CONV macro below is for non-ASCII machines.
59 It converts digest text to ASCII so the MD5 will be correct for
60 what ultimately goes over the network.
62 #define CURL_OUTPUT_DIGEST_CONV(a, b) \
63 result = Curl_convert_to_network(a, (char *)b, strlen((const char *)b)); \
68 #endif /* !USE_WINDOWS_SSPI */
70 bool Curl_auth_digest_get_pair(const char *str, char *value, char *content,
74 bool starts_with_quote = FALSE;
77 for(c = DIGEST_MAX_VALUE_LENGTH - 1; (*str && (*str != '=') && c--);)
86 /* This starts with a quote so it must end with one as well! */
88 starts_with_quote = TRUE;
91 for(c = DIGEST_MAX_CONTENT_LENGTH - 1; *str && c--; str++) {
95 /* possibly the start of an escaped quote */
97 *content++ = '\\'; /* Even though this is an escape character, we still
98 store it as-is in the target buffer */
104 if(!starts_with_quote) {
105 /* This signals the end of the content if we didn't get a starting
106 quote and then we do "sloppy" parsing */
119 if(!escape && starts_with_quote) {
137 #if !defined(USE_WINDOWS_SSPI)
138 /* Convert md5 chunk to RFC2617 (section 3.1.3) -suitable ascii string*/
139 static void auth_digest_md5_to_ascii(unsigned char *source, /* 16 bytes */
140 unsigned char *dest) /* 33 bytes */
143 for(i = 0; i < 16; i++)
144 snprintf((char *) &dest[i * 2], 3, "%02x", source[i]);
147 /* Perform quoted-string escaping as described in RFC2616 and its errata */
148 static char *auth_digest_string_quoted(const char *source)
151 const char *s = source;
152 size_t n = 1; /* null terminator */
154 /* Calculate size needed */
157 if(*s == '"' || *s == '\\') {
168 if(*s == '"' || *s == '\\') {
179 /* Retrieves the value for a corresponding key from the challenge string
180 * returns TRUE if the key could be found, FALSE if it does not exists
182 static bool auth_digest_get_key_value(const char *chlg,
191 find_pos = strstr(chlg, key);
195 find_pos += strlen(key);
197 for(i = 0; *find_pos && *find_pos != end_char && i < max_val_len - 1; ++i)
198 value[i] = *find_pos++;
204 static CURLcode auth_digest_get_qop_values(const char *options, int *value)
210 /* Initialise the output */
213 /* Tokenise the list of qop values. Use a temporary clone of the buffer since
214 strtok_r() ruins it. */
215 tmp = strdup(options);
217 return CURLE_OUT_OF_MEMORY;
219 token = strtok_r(tmp, ",", &tok_buf);
220 while(token != NULL) {
221 if(strcasecompare(token, DIGEST_QOP_VALUE_STRING_AUTH))
222 *value |= DIGEST_QOP_VALUE_AUTH;
223 else if(strcasecompare(token, DIGEST_QOP_VALUE_STRING_AUTH_INT))
224 *value |= DIGEST_QOP_VALUE_AUTH_INT;
225 else if(strcasecompare(token, DIGEST_QOP_VALUE_STRING_AUTH_CONF))
226 *value |= DIGEST_QOP_VALUE_AUTH_CONF;
228 token = strtok_r(NULL, ",", &tok_buf);
237 * auth_decode_digest_md5_message()
239 * This is used internally to decode an already encoded DIGEST-MD5 challenge
240 * message into the seperate attributes.
244 * chlg64 [in] - The base64 encoded challenge message.
245 * nonce [in/out] - The buffer where the nonce will be stored.
246 * nlen [in] - The length of the nonce buffer.
247 * realm [in/out] - The buffer where the realm will be stored.
248 * rlen [in] - The length of the realm buffer.
249 * alg [in/out] - The buffer where the algorithm will be stored.
250 * alen [in] - The length of the algorithm buffer.
251 * qop [in/out] - The buffer where the qop-options will be stored.
252 * qlen [in] - The length of the qop buffer.
254 * Returns CURLE_OK on success.
256 static CURLcode auth_decode_digest_md5_message(const char *chlg64,
257 char *nonce, size_t nlen,
258 char *realm, size_t rlen,
259 char *alg, size_t alen,
260 char *qop, size_t qlen)
262 CURLcode result = CURLE_OK;
263 unsigned char *chlg = NULL;
265 size_t chlg64len = strlen(chlg64);
267 /* Decode the base-64 encoded challenge message */
268 if(chlg64len && *chlg64 != '=') {
269 result = Curl_base64_decode(chlg64, &chlg, &chlglen);
274 /* Ensure we have a valid challenge message */
276 return CURLE_BAD_CONTENT_ENCODING;
278 /* Retrieve nonce string from the challenge */
279 if(!auth_digest_get_key_value((char *) chlg, "nonce=\"", nonce, nlen,
282 return CURLE_BAD_CONTENT_ENCODING;
285 /* Retrieve realm string from the challenge */
286 if(!auth_digest_get_key_value((char *) chlg, "realm=\"", realm, rlen,
288 /* Challenge does not have a realm, set empty string [RFC2831] page 6 */
292 /* Retrieve algorithm string from the challenge */
293 if(!auth_digest_get_key_value((char *) chlg, "algorithm=", alg, alen, ',')) {
295 return CURLE_BAD_CONTENT_ENCODING;
298 /* Retrieve qop-options string from the challenge */
299 if(!auth_digest_get_key_value((char *) chlg, "qop=\"", qop, qlen, '\"')) {
301 return CURLE_BAD_CONTENT_ENCODING;
310 * Curl_auth_is_digest_supported()
312 * This is used to evaluate if DIGEST is supported.
316 * Returns TRUE as DIGEST as handled by libcurl.
318 bool Curl_auth_is_digest_supported(void)
324 * Curl_auth_create_digest_md5_message()
326 * This is used to generate an already encoded DIGEST-MD5 response message
327 * ready for sending to the recipient.
331 * data [in] - The session handle.
332 * chlg64 [in] - The base64 encoded challenge message.
333 * userp [in] - The user name.
334 * passdwp [in] - The user's password.
335 * service [in] - The service type such as http, smtp, pop or imap.
336 * outptr [in/out] - The address where a pointer to newly allocated memory
337 * holding the result will be stored upon completion.
338 * outlen [out] - The length of the output message.
340 * Returns CURLE_OK on success.
342 CURLcode Curl_auth_create_digest_md5_message(struct Curl_easy *data,
347 char **outptr, size_t *outlen)
349 CURLcode result = CURLE_OK;
352 char *response = NULL;
353 unsigned char digest[MD5_DIGEST_LEN];
354 char HA1_hex[2 * MD5_DIGEST_LEN + 1];
355 char HA2_hex[2 * MD5_DIGEST_LEN + 1];
356 char resp_hash_hex[2 * MD5_DIGEST_LEN + 1];
360 char qop_options[64];
363 unsigned int entropy[4];
364 char nonceCount[] = "00000001";
365 char method[] = "AUTHENTICATE";
366 char qop[] = DIGEST_QOP_VALUE_STRING_AUTH;
369 /* Decode the challange message */
370 result = auth_decode_digest_md5_message(chlg64, nonce, sizeof(nonce),
371 realm, sizeof(realm),
372 algorithm, sizeof(algorithm),
373 qop_options, sizeof(qop_options));
377 /* We only support md5 sessions */
378 if(strcmp(algorithm, "md5-sess") != 0)
379 return CURLE_BAD_CONTENT_ENCODING;
381 /* Get the qop-values from the qop-options */
382 result = auth_digest_get_qop_values(qop_options, &qop_values);
386 /* We only support auth quality-of-protection */
387 if(!(qop_values & DIGEST_QOP_VALUE_AUTH))
388 return CURLE_BAD_CONTENT_ENCODING;
390 /* Generate 16 bytes of random data */
391 result = Curl_rand(data, &entropy[0], 4);
395 /* Convert the random data into a 32 byte hex string */
396 snprintf(cnonce, sizeof(cnonce), "%08x%08x%08x%08x",
397 entropy[0], entropy[1], entropy[2], entropy[3]);
399 /* So far so good, now calculate A1 and H(A1) according to RFC 2831 */
400 ctxt = Curl_MD5_init(Curl_DIGEST_MD5);
402 return CURLE_OUT_OF_MEMORY;
404 Curl_MD5_update(ctxt, (const unsigned char *) userp,
405 curlx_uztoui(strlen(userp)));
406 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
407 Curl_MD5_update(ctxt, (const unsigned char *) realm,
408 curlx_uztoui(strlen(realm)));
409 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
410 Curl_MD5_update(ctxt, (const unsigned char *) passwdp,
411 curlx_uztoui(strlen(passwdp)));
412 Curl_MD5_final(ctxt, digest);
414 ctxt = Curl_MD5_init(Curl_DIGEST_MD5);
416 return CURLE_OUT_OF_MEMORY;
418 Curl_MD5_update(ctxt, (const unsigned char *) digest, MD5_DIGEST_LEN);
419 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
420 Curl_MD5_update(ctxt, (const unsigned char *) nonce,
421 curlx_uztoui(strlen(nonce)));
422 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
423 Curl_MD5_update(ctxt, (const unsigned char *) cnonce,
424 curlx_uztoui(strlen(cnonce)));
425 Curl_MD5_final(ctxt, digest);
427 /* Convert calculated 16 octet hex into 32 bytes string */
428 for(i = 0; i < MD5_DIGEST_LEN; i++)
429 snprintf(&HA1_hex[2 * i], 3, "%02x", digest[i]);
431 /* Generate our SPN */
432 spn = Curl_auth_build_spn(service, realm, NULL);
434 return CURLE_OUT_OF_MEMORY;
436 /* Calculate H(A2) */
437 ctxt = Curl_MD5_init(Curl_DIGEST_MD5);
441 return CURLE_OUT_OF_MEMORY;
444 Curl_MD5_update(ctxt, (const unsigned char *) method,
445 curlx_uztoui(strlen(method)));
446 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
447 Curl_MD5_update(ctxt, (const unsigned char *) spn,
448 curlx_uztoui(strlen(spn)));
449 Curl_MD5_final(ctxt, digest);
451 for(i = 0; i < MD5_DIGEST_LEN; i++)
452 snprintf(&HA2_hex[2 * i], 3, "%02x", digest[i]);
454 /* Now calculate the response hash */
455 ctxt = Curl_MD5_init(Curl_DIGEST_MD5);
459 return CURLE_OUT_OF_MEMORY;
462 Curl_MD5_update(ctxt, (const unsigned char *) HA1_hex, 2 * MD5_DIGEST_LEN);
463 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
464 Curl_MD5_update(ctxt, (const unsigned char *) nonce,
465 curlx_uztoui(strlen(nonce)));
466 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
468 Curl_MD5_update(ctxt, (const unsigned char *) nonceCount,
469 curlx_uztoui(strlen(nonceCount)));
470 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
471 Curl_MD5_update(ctxt, (const unsigned char *) cnonce,
472 curlx_uztoui(strlen(cnonce)));
473 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
474 Curl_MD5_update(ctxt, (const unsigned char *) qop,
475 curlx_uztoui(strlen(qop)));
476 Curl_MD5_update(ctxt, (const unsigned char *) ":", 1);
478 Curl_MD5_update(ctxt, (const unsigned char *) HA2_hex, 2 * MD5_DIGEST_LEN);
479 Curl_MD5_final(ctxt, digest);
481 for(i = 0; i < MD5_DIGEST_LEN; i++)
482 snprintf(&resp_hash_hex[2 * i], 3, "%02x", digest[i]);
484 /* Generate the response */
485 response = aprintf("username=\"%s\",realm=\"%s\",nonce=\"%s\","
486 "cnonce=\"%s\",nc=\"%s\",digest-uri=\"%s\",response=%s,"
489 cnonce, nonceCount, spn, resp_hash_hex, qop);
492 return CURLE_OUT_OF_MEMORY;
494 /* Base64 encode the response */
495 result = Curl_base64_encode(data, response, 0, outptr, outlen);
503 * Curl_auth_decode_digest_http_message()
505 * This is used to decode a HTTP DIGEST challenge message into the seperate
510 * chlg [in] - The challenge message.
511 * digest [in/out] - The digest data struct being used and modified.
513 * Returns CURLE_OK on success.
515 CURLcode Curl_auth_decode_digest_http_message(const char *chlg,
516 struct digestdata *digest)
518 bool before = FALSE; /* got a nonce before */
519 bool foundAuth = FALSE;
520 bool foundAuthInt = FALSE;
524 /* If we already have received a nonce, keep that in mind */
528 /* Clean up any former leftovers and initialise to defaults */
529 Curl_auth_digest_cleanup(digest);
532 char value[DIGEST_MAX_VALUE_LENGTH];
533 char content[DIGEST_MAX_CONTENT_LENGTH];
535 /* Pass all additional spaces here */
536 while(*chlg && ISSPACE(*chlg))
539 /* Extract a value=content pair */
540 if(Curl_auth_digest_get_pair(chlg, value, content, &chlg)) {
541 if(strcasecompare(value, "nonce")) {
543 digest->nonce = strdup(content);
545 return CURLE_OUT_OF_MEMORY;
547 else if(strcasecompare(value, "stale")) {
548 if(strcasecompare(content, "true")) {
549 digest->stale = TRUE;
550 digest->nc = 1; /* we make a new nonce now */
553 else if(strcasecompare(value, "realm")) {
555 digest->realm = strdup(content);
557 return CURLE_OUT_OF_MEMORY;
559 else if(strcasecompare(value, "opaque")) {
560 free(digest->opaque);
561 digest->opaque = strdup(content);
563 return CURLE_OUT_OF_MEMORY;
565 else if(strcasecompare(value, "qop")) {
567 /* Tokenize the list and choose auth if possible, use a temporary
568 clone of the buffer since strtok_r() ruins it */
569 tmp = strdup(content);
571 return CURLE_OUT_OF_MEMORY;
573 token = strtok_r(tmp, ",", &tok_buf);
574 while(token != NULL) {
575 if(strcasecompare(token, DIGEST_QOP_VALUE_STRING_AUTH)) {
578 else if(strcasecompare(token, DIGEST_QOP_VALUE_STRING_AUTH_INT)) {
581 token = strtok_r(NULL, ",", &tok_buf);
586 /* Select only auth or auth-int. Otherwise, ignore */
589 digest->qop = strdup(DIGEST_QOP_VALUE_STRING_AUTH);
591 return CURLE_OUT_OF_MEMORY;
593 else if(foundAuthInt) {
595 digest->qop = strdup(DIGEST_QOP_VALUE_STRING_AUTH_INT);
597 return CURLE_OUT_OF_MEMORY;
600 else if(strcasecompare(value, "algorithm")) {
601 free(digest->algorithm);
602 digest->algorithm = strdup(content);
603 if(!digest->algorithm)
604 return CURLE_OUT_OF_MEMORY;
606 if(strcasecompare(content, "MD5-sess"))
607 digest->algo = CURLDIGESTALGO_MD5SESS;
608 else if(strcasecompare(content, "MD5"))
609 digest->algo = CURLDIGESTALGO_MD5;
611 return CURLE_BAD_CONTENT_ENCODING;
614 /* Unknown specifier, ignore it! */
618 break; /* We're done here */
620 /* Pass all additional spaces here */
621 while(*chlg && ISSPACE(*chlg))
624 /* Allow the list to be comma-separated */
629 /* We had a nonce since before, and we got another one now without
630 'stale=true'. This means we provided bad credentials in the previous
632 if(before && !digest->stale)
633 return CURLE_BAD_CONTENT_ENCODING;
635 /* We got this header without a nonce, that's a bad Digest line! */
637 return CURLE_BAD_CONTENT_ENCODING;
643 * Curl_auth_create_digest_http_message()
645 * This is used to generate a HTTP DIGEST response message ready for sending
650 * data [in] - The session handle.
651 * userp [in] - The user name.
652 * passdwp [in] - The user's password.
653 * request [in] - The HTTP request.
654 * uripath [in] - The path of the HTTP uri.
655 * digest [in/out] - The digest data struct being used and modified.
656 * outptr [in/out] - The address where a pointer to newly allocated memory
657 * holding the result will be stored upon completion.
658 * outlen [out] - The length of the output message.
660 * Returns CURLE_OK on success.
662 CURLcode Curl_auth_create_digest_http_message(struct Curl_easy *data,
665 const unsigned char *request,
666 const unsigned char *uripath,
667 struct digestdata *digest,
668 char **outptr, size_t *outlen)
671 unsigned char md5buf[16]; /* 16 bytes/128 bits */
672 unsigned char request_digest[33];
673 unsigned char *md5this;
674 unsigned char ha1[33]; /* 32 digits and 1 zero byte */
675 unsigned char ha2[33]; /* 32 digits and 1 zero byte */
678 size_t cnonce_sz = 0;
680 char *response = NULL;
686 if(!digest->cnonce) {
688 result = Curl_rand(data, &rnd[0], 4);
691 snprintf(cnoncebuf, sizeof(cnoncebuf), "%08x%08x%08x%08x",
692 rnd[0], rnd[1], rnd[2], rnd[3]);
694 result = Curl_base64_encode(data, cnoncebuf, strlen(cnoncebuf),
695 &cnonce, &cnonce_sz);
699 digest->cnonce = cnonce;
703 If the algorithm is "MD5" or unspecified (which then defaults to MD5):
705 A1 = unq(username-value) ":" unq(realm-value) ":" passwd
707 If the algorithm is "MD5-sess" then:
709 A1 = H(unq(username-value) ":" unq(realm-value) ":" passwd) ":"
710 unq(nonce-value) ":" unq(cnonce-value)
713 md5this = (unsigned char *)
714 aprintf("%s:%s:%s", userp, digest->realm, passwdp);
716 return CURLE_OUT_OF_MEMORY;
718 CURL_OUTPUT_DIGEST_CONV(data, md5this); /* convert on non-ASCII machines */
719 Curl_md5it(md5buf, md5this);
721 auth_digest_md5_to_ascii(md5buf, ha1);
723 if(digest->algo == CURLDIGESTALGO_MD5SESS) {
724 /* nonce and cnonce are OUTSIDE the hash */
725 tmp = aprintf("%s:%s:%s", ha1, digest->nonce, digest->cnonce);
727 return CURLE_OUT_OF_MEMORY;
729 CURL_OUTPUT_DIGEST_CONV(data, tmp); /* Convert on non-ASCII machines */
730 Curl_md5it(md5buf, (unsigned char *) tmp);
732 auth_digest_md5_to_ascii(md5buf, ha1);
736 If the "qop" directive's value is "auth" or is unspecified, then A2 is:
738 A2 = Method ":" digest-uri-value
740 If the "qop" value is "auth-int", then A2 is:
742 A2 = Method ":" digest-uri-value ":" H(entity-body)
744 (The "Method" value is the HTTP request method as specified in section
748 md5this = (unsigned char *) aprintf("%s:%s", request, uripath);
750 if(digest->qop && strcasecompare(digest->qop, "auth-int")) {
751 /* We don't support auth-int for PUT or POST at the moment.
752 TODO: replace md5 of empty string with entity-body for PUT/POST */
753 unsigned char *md5this2 = (unsigned char *)
754 aprintf("%s:%s", md5this, "d41d8cd98f00b204e9800998ecf8427e");
760 return CURLE_OUT_OF_MEMORY;
762 CURL_OUTPUT_DIGEST_CONV(data, md5this); /* convert on non-ASCII machines */
763 Curl_md5it(md5buf, md5this);
765 auth_digest_md5_to_ascii(md5buf, ha2);
768 md5this = (unsigned char *) aprintf("%s:%s:%08x:%s:%s:%s",
777 md5this = (unsigned char *) aprintf("%s:%s:%s",
784 return CURLE_OUT_OF_MEMORY;
786 CURL_OUTPUT_DIGEST_CONV(data, md5this); /* convert on non-ASCII machines */
787 Curl_md5it(md5buf, md5this);
789 auth_digest_md5_to_ascii(md5buf, request_digest);
791 /* For test case 64 (snooped from a Mozilla 1.3a request)
793 Authorization: Digest username="testuser", realm="testrealm", \
794 nonce="1053604145", uri="/64", response="c55f7f30d83d774a3d2dcacf725abaca"
796 Digest parameters are all quoted strings. Username which is provided by
797 the user will need double quotes and backslashes within it escaped. For
798 the other fields, this shouldn't be an issue. realm, nonce, and opaque
799 are copied as is from the server, escapes and all. cnonce is generated
800 with web-safe characters. uri is already percent encoded. nc is 8 hex
801 characters. algorithm and qop with standard values only contain web-safe
804 userp_quoted = auth_digest_string_quoted(userp);
806 return CURLE_OUT_OF_MEMORY;
809 response = aprintf("username=\"%s\", "
826 if(strcasecompare(digest->qop, "auth"))
827 digest->nc++; /* The nc (from RFC) has to be a 8 hex digit number 0
828 padded which tells to the server how many times you are
829 using the same nonce in the qop=auth mode */
832 response = aprintf("username=\"%s\", "
845 return CURLE_OUT_OF_MEMORY;
847 /* Add the optional fields */
849 /* Append the opaque */
850 tmp = aprintf("%s, opaque=\"%s\"", response, digest->opaque);
853 return CURLE_OUT_OF_MEMORY;
858 if(digest->algorithm) {
859 /* Append the algorithm */
860 tmp = aprintf("%s, algorithm=\"%s\"", response, digest->algorithm);
863 return CURLE_OUT_OF_MEMORY;
868 /* Return the output */
870 *outlen = strlen(response);
876 * Curl_auth_digest_cleanup()
878 * This is used to clean up the digest specific data.
882 * digest [in/out] - The digest data struct being cleaned up.
885 void Curl_auth_digest_cleanup(struct digestdata *digest)
887 Curl_safefree(digest->nonce);
888 Curl_safefree(digest->cnonce);
889 Curl_safefree(digest->realm);
890 Curl_safefree(digest->opaque);
891 Curl_safefree(digest->qop);
892 Curl_safefree(digest->algorithm);
895 digest->algo = CURLDIGESTALGO_MD5; /* default algorithm */
896 digest->stale = FALSE; /* default means normal, not stale */
898 #endif /* !USE_WINDOWS_SSPI */
900 #endif /* CURL_DISABLE_CRYPTO_AUTH */