/* This file is part of libmicrohttpd Copyright (C) 2010, 2011, 2012 Daniel Pittman and Christian Grothoff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * @file digestauth.c * @brief Implements HTTP digest authentication * @author Amr Ali * @author Matthieu Speder */ #include "platform.h" #include #include "internal.h" #include "md5.h" #if defined(_WIN32) && defined(MHD_W32_MUTEX_) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN 1 #endif /* !WIN32_LEAN_AND_MEAN */ #include #endif /* _WIN32 && MHD_W32_MUTEX_ */ #define HASH_MD5_HEX_LEN (2 * MD5_DIGEST_SIZE) /** * Beginning string for any valid Digest authentication header. */ #define _BASE "Digest " /** * Maximum length of a username for digest authentication. */ #define MAX_USERNAME_LENGTH 128 /** * Maximum length of a realm for digest authentication. */ #define MAX_REALM_LENGTH 256 /** * Maximum length of the response in digest authentication. */ #define MAX_AUTH_RESPONSE_LENGTH 128 /** * convert bin to hex * * @param bin binary data * @param len number of bytes in bin * @param hex pointer to len*2+1 bytes */ static void cvthex (const unsigned char *bin, size_t len, char *hex) { size_t i; unsigned int j; for (i = 0; i < len; ++i) { j = (bin[i] >> 4) & 0x0f; hex[i * 2] = j <= 9 ? (j + '0') : (j + 'a' - 10); j = bin[i] & 0x0f; hex[i * 2 + 1] = j <= 9 ? (j + '0') : (j + 'a' - 10); } hex[len * 2] = '\0'; } /** * calculate H(A1) as per RFC2617 spec and store the * result in 'sessionkey'. * * @param alg The hash algorithm used, can be "md5" or "md5-sess" * @param username A `char *' pointer to the username value * @param realm A `char *' pointer to the realm value * @param password A `char *' pointer to the password value * @param nonce A `char *' pointer to the nonce value * @param cnonce A `char *' pointer to the cnonce value * @param sessionkey pointer to buffer of HASH_MD5_HEX_LEN+1 bytes */ static void digest_calc_ha1 (const char *alg, const char *username, const char *realm, const char *password, const char *nonce, const char *cnonce, char *sessionkey) { struct MD5Context md5; unsigned char ha1[MD5_DIGEST_SIZE]; MD5Init (&md5); MD5Update (&md5, username, strlen (username)); MD5Update (&md5, ":", 1); MD5Update (&md5, realm, strlen (realm)); MD5Update (&md5, ":", 1); MD5Update (&md5, password, strlen (password)); MD5Final (ha1, &md5); if (MHD_str_equal_caseless_(alg, "md5-sess")) { MD5Init (&md5); MD5Update (&md5, ha1, sizeof (ha1)); MD5Update (&md5, ":", 1); MD5Update (&md5, nonce, strlen (nonce)); MD5Update (&md5, ":", 1); MD5Update (&md5, cnonce, strlen (cnonce)); MD5Final (ha1, &md5); } cvthex (ha1, sizeof (ha1), sessionkey); } /** * Calculate request-digest/response-digest as per RFC2617 spec * * @param ha1 H(A1) * @param nonce nonce from server * @param noncecount 8 hex digits * @param cnonce client nonce * @param qop qop-value: "", "auth" or "auth-int" * @param method method from request * @param uri requested URL * @param hentity H(entity body) if qop="auth-int" * @param response request-digest or response-digest */ static void digest_calc_response (const char *ha1, const char *nonce, const char *noncecount, const char *cnonce, const char *qop, const char *method, const char *uri, const char *hentity, char *response) { struct MD5Context md5; unsigned char ha2[MD5_DIGEST_SIZE]; unsigned char resphash[MD5_DIGEST_SIZE]; char ha2hex[HASH_MD5_HEX_LEN + 1]; MD5Init (&md5); MD5Update (&md5, method, strlen(method)); MD5Update (&md5, ":", 1); MD5Update (&md5, uri, strlen(uri)); #if 0 if (0 == strcasecmp(qop, "auth-int")) { /* This is dead code since the rest of this module does not support auth-int. */ MD5Update (&md5, ":", 1); if (NULL != hentity) MD5Update (&md5, hentity, strlen(hentity)); } #endif MD5Final (ha2, &md5); cvthex (ha2, MD5_DIGEST_SIZE, ha2hex); MD5Init (&md5); /* calculate response */ MD5Update (&md5, ha1, HASH_MD5_HEX_LEN); MD5Update (&md5, ":", 1); MD5Update (&md5, nonce, strlen(nonce)); MD5Update (&md5, ":", 1); if ('\0' != *qop) { MD5Update (&md5, noncecount, strlen(noncecount)); MD5Update (&md5, ":", 1); MD5Update (&md5, cnonce, strlen(cnonce)); MD5Update (&md5, ":", 1); MD5Update (&md5, qop, strlen(qop)); MD5Update (&md5, ":", 1); } MD5Update (&md5, ha2hex, HASH_MD5_HEX_LEN); MD5Final (resphash, &md5); cvthex (resphash, sizeof (resphash), response); } /** * Lookup subvalue off of the HTTP Authorization header. * * A description of the input format for 'data' is at * http://en.wikipedia.org/wiki/Digest_access_authentication * * * @param dest where to store the result (possibly truncated if * the buffer is not big enough). * @param size size of dest * @param data pointer to the Authorization header * @param key key to look up in data * @return size of the located value, 0 if otherwise */ static size_t lookup_sub_value (char *dest, size_t size, const char *data, const char *key) { size_t keylen; size_t len; const char *ptr; const char *eq; const char *q1; const char *q2; const char *qn; if (0 == size) return 0; keylen = strlen (key); ptr = data; while ('\0' != *ptr) { if (NULL == (eq = strchr (ptr, '='))) return 0; q1 = eq + 1; while (' ' == *q1) q1++; if ('\"' != *q1) { q2 = strchr (q1, ','); qn = q2; } else { q1++; q2 = strchr (q1, '\"'); if (NULL == q2) return 0; /* end quote not found */ qn = q2 + 1; } if ((MHD_str_equal_caseless_n_(ptr, key, keylen)) && (eq == &ptr[keylen]) ) { if (NULL == q2) { len = strlen (q1) + 1; if (size > len) size = len; size--; strncpy (dest, q1, size); dest[size] = '\0'; return size; } else { if (size > (size_t) ((q2 - q1) + 1)) size = (q2 - q1) + 1; size--; memcpy (dest, q1, size); dest[size] = '\0'; return size; } } if (NULL == qn) return 0; ptr = strchr (qn, ','); if (NULL == ptr) return 0; ptr++; while (' ' == *ptr) ptr++; } return 0; } /** * Check nonce-nc map array with either new nonce counter * or a whole new nonce. * * @param connection The MHD connection structure * @param nonce A pointer that referenced a zero-terminated array of nonce * @param nc The nonce counter, zero to add the nonce to the array * @return MHD_YES if successful, MHD_NO if invalid (or we have no NC array) */ static int check_nonce_nc (struct MHD_Connection *connection, const char *nonce, unsigned long int nc) { uint32_t off; uint32_t mod; const char *np; mod = connection->daemon->nonce_nc_size; if (0 == mod) return MHD_NO; /* no array! */ /* super-fast xor-based "hash" function for HT lookup in nonce array */ off = 0; np = nonce; while ('\0' != *np) { off = (off << 8) | (*np ^ (off >> 24)); np++; } off = off % mod; /* * Look for the nonce, if it does exist and its corresponding * nonce counter is less than the current nonce counter by 1, * then only increase the nonce counter by one. */ (void) MHD_mutex_lock_ (&connection->daemon->nnc_lock); if (0 == nc) { strcpy(connection->daemon->nnc[off].nonce, nonce); connection->daemon->nnc[off].nc = 0; (void) MHD_mutex_unlock_ (&connection->daemon->nnc_lock); return MHD_YES; } if ( (nc <= connection->daemon->nnc[off].nc) || (0 != strcmp(connection->daemon->nnc[off].nonce, nonce)) ) { (void) MHD_mutex_unlock_ (&connection->daemon->nnc_lock); #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Stale nonce received. If this happens a lot, you should probably increase the size of the nonce array.\n"); #endif return MHD_NO; } connection->daemon->nnc[off].nc = nc; (void) MHD_mutex_unlock_ (&connection->daemon->nnc_lock); return MHD_YES; } /** * Get the username from the authorization header sent by the client * * @param connection The MHD connection structure * @return NULL if no username could be found, a pointer * to the username if found * @ingroup authentication */ char * MHD_digest_auth_get_username(struct MHD_Connection *connection) { size_t len; char user[MAX_USERNAME_LENGTH]; const char *header; if (NULL == (header = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_AUTHORIZATION))) return NULL; if (0 != strncmp (header, _BASE, strlen (_BASE))) return NULL; header += strlen (_BASE); if (0 == (len = lookup_sub_value (user, sizeof (user), header, "username"))) return NULL; return strdup (user); } /** * Calculate the server nonce so that it mitigates replay attacks * The current format of the nonce is ... * H(timestamp ":" method ":" random ":" uri ":" realm) + Hex(timestamp) * * @param nonce_time The amount of time in seconds for a nonce to be invalid * @param method HTTP method * @param rnd A pointer to a character array for the random seed * @param rnd_size The size of the random seed array @a rnd * @param uri HTTP URI (in MHD, without the arguments ("?k=v") * @param realm A string of characters that describes the realm of auth. * @param nonce A pointer to a character array for the nonce to put in */ static void calculate_nonce (uint32_t nonce_time, const char *method, const char *rnd, size_t rnd_size, const char *uri, const char *realm, char *nonce) { struct MD5Context md5; unsigned char timestamp[4]; unsigned char tmpnonce[MD5_DIGEST_SIZE]; char timestamphex[sizeof(timestamp) * 2 + 1]; MD5Init (&md5); timestamp[0] = (nonce_time & 0xff000000) >> 0x18; timestamp[1] = (nonce_time & 0x00ff0000) >> 0x10; timestamp[2] = (nonce_time & 0x0000ff00) >> 0x08; timestamp[3] = (nonce_time & 0x000000ff); MD5Update (&md5, timestamp, 4); MD5Update (&md5, ":", 1); MD5Update (&md5, method, strlen (method)); MD5Update (&md5, ":", 1); if (rnd_size > 0) MD5Update (&md5, rnd, rnd_size); MD5Update (&md5, ":", 1); MD5Update (&md5, uri, strlen (uri)); MD5Update (&md5, ":", 1); MD5Update (&md5, realm, strlen (realm)); MD5Final (tmpnonce, &md5); cvthex (tmpnonce, sizeof (tmpnonce), nonce); cvthex (timestamp, 4, timestamphex); strncat (nonce, timestamphex, 8); } /** * Test if the given key-value pair is in the headers for the * given connection. * * @param connection the connection * @param key the key * @param value the value, can be NULL * @return #MHD_YES if the key-value pair is in the headers, * #MHD_NO if not */ static int test_header (struct MHD_Connection *connection, const char *key, const char *value) { struct MHD_HTTP_Header *pos; for (pos = connection->headers_received; NULL != pos; pos = pos->next) { if (MHD_GET_ARGUMENT_KIND != pos->kind) continue; if (0 != strcmp (key, pos->header)) continue; if ( (NULL == value) && (NULL == pos->value) ) return MHD_YES; if ( (NULL == value) || (NULL == pos->value) || (0 != strcmp (value, pos->value)) ) continue; return MHD_YES; } return MHD_NO; } /** * Check that the arguments given by the client as part * of the authentication header match the arguments we * got as part of the HTTP request URI. * * @param connection connections with headers to compare against * @param args argument URI string (after "?" in URI) * @return MHD_YES if the arguments match, * MHD_NO if not */ static int check_argument_match (struct MHD_Connection *connection, const char *args) { struct MHD_HTTP_Header *pos; char *argb; char *argp; char *equals; char *amper; unsigned int num_headers; argb = strdup(args); if (NULL == argb) { #if HAVE_MESSAGES MHD_DLOG(connection->daemon, "Failed to allocate memory for copy of URI arguments\n"); #endif /* HAVE_MESSAGES */ return MHD_NO; } num_headers = 0; argp = argb; while ( (NULL != argp) && ('\0' != argp[0]) ) { equals = strchr (argp, '='); if (NULL == equals) { /* add with 'value' NULL */ connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls, connection, argp); if (MHD_YES != test_header (connection, argp, NULL)) return MHD_NO; num_headers++; break; } equals[0] = '\0'; equals++; amper = strchr (equals, '&'); if (NULL != amper) { amper[0] = '\0'; amper++; } connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls, connection, argp); connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls, connection, equals); if (! test_header (connection, argp, equals)) return MHD_NO; num_headers++; argp = amper; } /* also check that the number of headers matches */ for (pos = connection->headers_received; NULL != pos; pos = pos->next) { if (MHD_GET_ARGUMENT_KIND != pos->kind) continue; num_headers--; } if (0 != num_headers) return MHD_NO; return MHD_YES; } /** * Authenticates the authorization header sent by the client * * @param connection The MHD connection structure * @param realm The realm presented to the client * @param username The username needs to be authenticated * @param password The password used in the authentication * @param nonce_timeout The amount of time for a nonce to be * invalid in seconds * @return #MHD_YES if authenticated, #MHD_NO if not, * #MHD_INVALID_NONCE if nonce is invalid * @ingroup authentication */ int MHD_digest_auth_check (struct MHD_Connection *connection, const char *realm, const char *username, const char *password, unsigned int nonce_timeout) { size_t len; const char *header; char *end; char nonce[MAX_NONCE_LENGTH]; char cnonce[MAX_NONCE_LENGTH]; char qop[15]; /* auth,auth-int */ char nc[20]; char response[MAX_AUTH_RESPONSE_LENGTH]; const char *hentity = NULL; /* "auth-int" is not supported */ char ha1[HASH_MD5_HEX_LEN + 1]; char respexp[HASH_MD5_HEX_LEN + 1]; char noncehashexp[HASH_MD5_HEX_LEN + 9]; uint32_t nonce_time; uint32_t t; size_t left; /* number of characters left in 'header' for 'uri' */ unsigned long int nci; header = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_AUTHORIZATION); if (NULL == header) return MHD_NO; if (0 != strncmp(header, _BASE, strlen(_BASE))) return MHD_NO; header += strlen (_BASE); left = strlen (header); { char un[MAX_USERNAME_LENGTH]; len = lookup_sub_value (un, sizeof (un), header, "username"); if ( (0 == len) || (0 != strcmp(username, un)) ) return MHD_NO; left -= strlen ("username") + len; } { char r[MAX_REALM_LENGTH]; len = lookup_sub_value(r, sizeof (r), header, "realm"); if ( (0 == len) || (0 != strcmp(realm, r)) ) return MHD_NO; left -= strlen ("realm") + len; } if (0 == (len = lookup_sub_value (nonce, sizeof (nonce), header, "nonce"))) return MHD_NO; left -= strlen ("nonce") + len; if (left > 32 * 1024) { /* we do not permit URIs longer than 32k, as we want to make sure to not blow our stack (or per-connection heap memory limit). Besides, 32k is already insanely large, but of course in theory the #MHD_OPTION_CONNECTION_MEMORY_LIMIT might be very large and would thus permit sending a >32k authorization header value. */ return MHD_NO; } { char *uri; uri = malloc(left + 1); if (NULL == uri) { #if HAVE_MESSAGES MHD_DLOG(connection->daemon, "Failed to allocate memory for auth header processing\n"); #endif /* HAVE_MESSAGES */ return MHD_NO; } if (0 == lookup_sub_value (uri, left + 1, header, "uri")) { free(uri); return MHD_NO; } /* 8 = 4 hexadecimal numbers for the timestamp */ nonce_time = strtoul (nonce + len - 8, (char **)NULL, 16); t = (uint32_t) MHD_monotonic_time(); /* * First level vetting for the nonce validity: if the timestamp * attached to the nonce exceeds `nonce_timeout', then the nonce is * invalid. */ if ( (t > nonce_time + nonce_timeout) || (nonce_time + nonce_timeout < nonce_time) ) { free(uri); return MHD_INVALID_NONCE; } if (0 != strncmp (uri, connection->url, strlen (connection->url))) { #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Authentication failed, URI does not match.\n"); #endif free(uri); return MHD_NO; } { const char *args = strchr (uri, '?'); if (NULL == args) args = ""; else args++; if (MHD_YES != check_argument_match (connection, args) ) { #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Authentication failed, arguments do not match.\n"); #endif free(uri); return MHD_NO; } } calculate_nonce (nonce_time, connection->method, connection->daemon->digest_auth_random, connection->daemon->digest_auth_rand_size, connection->url, realm, noncehashexp); /* * Second level vetting for the nonce validity * if the timestamp attached to the nonce is valid * and possibly fabricated (in case of an attack) * the attacker must also know the random seed to be * able to generate a "sane" nonce, which if he does * not, the nonce fabrication process going to be * very hard to achieve. */ if (0 != strcmp (nonce, noncehashexp)) { free(uri); return MHD_INVALID_NONCE; } if ( (0 == lookup_sub_value (cnonce, sizeof (cnonce), header, "cnonce")) || (0 == lookup_sub_value (qop, sizeof (qop), header, "qop")) || ( (0 != strcmp (qop, "auth")) && (0 != strcmp (qop, "")) ) || (0 == lookup_sub_value (nc, sizeof (nc), header, "nc")) || (0 == lookup_sub_value (response, sizeof (response), header, "response")) ) { #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Authentication failed, invalid format.\n"); #endif free(uri); return MHD_NO; } nci = strtoul (nc, &end, 16); if ( ('\0' != *end) || ( (LONG_MAX == nci) && (ERANGE == errno) ) ) { #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Authentication failed, invalid format.\n"); #endif free(uri); return MHD_NO; /* invalid nonce format */ } /* * Checking if that combination of nonce and nc is sound * and not a replay attack attempt. Also adds the nonce * to the nonce-nc map if it does not exist there. */ if (MHD_YES != check_nonce_nc (connection, nonce, nci)) { free(uri); return MHD_NO; } digest_calc_ha1("md5", username, realm, password, nonce, cnonce, ha1); digest_calc_response (ha1, nonce, nc, cnonce, qop, connection->method, uri, hentity, respexp); free(uri); return (0 == strcmp(response, respexp)) ? MHD_YES : MHD_NO; } } /** * Queues a response to request authentication from the client * * @param connection The MHD connection structure * @param realm the realm presented to the client * @param opaque string to user for opaque value * @param response reply to send; should contain the "access denied" * body; note that this function will set the "WWW Authenticate" * header and that the caller should not do this * @param signal_stale #MHD_YES if the nonce is invalid to add * 'stale=true' to the authentication header * @return #MHD_YES on success, #MHD_NO otherwise * @ingroup authentication */ int MHD_queue_auth_fail_response (struct MHD_Connection *connection, const char *realm, const char *opaque, struct MHD_Response *response, int signal_stale) { int ret; size_t hlen; char nonce[HASH_MD5_HEX_LEN + 9]; /* Generating the server nonce */ calculate_nonce ((uint32_t) MHD_monotonic_time(), connection->method, connection->daemon->digest_auth_random, connection->daemon->digest_auth_rand_size, connection->url, realm, nonce); if (MHD_YES != check_nonce_nc (connection, nonce, 0)) { #if HAVE_MESSAGES MHD_DLOG (connection->daemon, "Could not register nonce (is the nonce array size zero?).\n"); #endif return MHD_NO; } /* Building the authentication header */ hlen = MHD_snprintf_(NULL, 0, "Digest realm=\"%s\",qop=\"auth\",nonce=\"%s\",opaque=\"%s\"%s", realm, nonce, opaque, signal_stale ? ",stale=\"true\"" : ""); { char *header; header = malloc(hlen + 1); if (NULL == header) { #if HAVE_MESSAGES MHD_DLOG(connection->daemon, "Failed to allocate memory for auth response header\n"); #endif /* HAVE_MESSAGES */ return MHD_NO; } MHD_snprintf_(header, hlen + 1, "Digest realm=\"%s\",qop=\"auth\",nonce=\"%s\",opaque=\"%s\"%s", realm, nonce, opaque, signal_stale ? ",stale=\"true\"" : ""); ret = MHD_add_response_header(response, MHD_HTTP_HEADER_WWW_AUTHENTICATE, header); free(header); } if (MHD_YES == ret) ret = MHD_queue_response(connection, MHD_HTTP_UNAUTHORIZED, response); return ret; } /* end of digestauth.c */