diff options
Diffstat (limited to 'src/microhttpd/digestauth.c')
-rw-r--r-- | src/microhttpd/digestauth.c | 871 |
1 files changed, 871 insertions, 0 deletions
diff --git a/src/microhttpd/digestauth.c b/src/microhttpd/digestauth.c new file mode 100644 index 00000000..9c3fe8c5 --- /dev/null +++ b/src/microhttpd/digestauth.c @@ -0,0 +1,871 @@ +/* + 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 <limits.h> +#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 <windows.h> +#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 */ |