diff options
Diffstat (limited to 'src/auth/mech-digest-md5.c')
-rw-r--r-- | src/auth/mech-digest-md5.c | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/src/auth/mech-digest-md5.c b/src/auth/mech-digest-md5.c new file mode 100644 index 0000000..d7f4383 --- /dev/null +++ b/src/auth/mech-digest-md5.c @@ -0,0 +1,592 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +/* Digest-MD5 SASL authentication, see RFC-2831 */ + +#include "auth-common.h" +#include "base64.h" +#include "buffer.h" +#include "hex-binary.h" +#include "mech-digest-md5-private.h" +#include "md5.h" +#include "randgen.h" +#include "str.h" +#include "str-sanitize.h" +#include "mech.h" +#include "passdb.h" + + +#define MAX_REALM_LEN 64 + +/* Linear whitespace */ +#define IS_LWS(c) ((c) == ' ' || (c) == '\t') + +static const char *qop_names[] = { "auth", "auth-int", "auth-conf" }; + +static string_t *get_digest_challenge(struct digest_auth_request *request) +{ + const struct auth_settings *set = request->auth_request.set; + buffer_t buf; + string_t *str; + const char *const *tmp; + unsigned char nonce[16]; + unsigned char nonce_base64[MAX_BASE64_ENCODED_SIZE(sizeof(nonce))+1]; + int i; + bool first_qop; + + /* + realm="hostname" (multiple allowed) + nonce="randomized data, at least 64bit" + qop="auth,auth-int,auth-conf" + maxbuf=number (with auth-int, auth-conf, defaults to 64k) + charset="utf-8" (iso-8859-1 if it doesn't exist) + algorithm="md5-sess" + cipher="3des,des,rc4-40,rc4,rc4-56" (with auth-conf) + */ + + /* get 128bit of random data as nonce */ + random_fill(nonce, sizeof(nonce)); + + buffer_create_from_data(&buf, nonce_base64, sizeof(nonce_base64)); + base64_encode(nonce, sizeof(nonce), &buf); + buffer_append_c(&buf, '\0'); + request->nonce = p_strdup(request->pool, buf.data); + + str = t_str_new(256); + if (*set->realms_arr == NULL) { + /* If no realms are given, at least Cyrus SASL client defaults + to destination host name */ + str_append(str, "realm=\"\","); + } else { + for (tmp = set->realms_arr; *tmp != NULL; tmp++) + str_printfa(str, "realm=\"%s\",", *tmp); + } + + str_printfa(str, "nonce=\"%s\",", request->nonce); + + str_append(str, "qop=\""); first_qop = TRUE; + for (i = 0; i < QOP_COUNT; i++) { + if ((request->qop & (1 << i)) != 0) { + if (first_qop) + first_qop = FALSE; + else + str_append_c(str, ','); + str_append(str, qop_names[i]); + } + } + str_append(str, "\","); + + str_append(str, "charset=\"utf-8\"," + "algorithm=\"md5-sess\""); + return str; +} + +static bool verify_credentials(struct digest_auth_request *request, + const unsigned char *credentials, size_t size) +{ + struct md5_context ctx; + unsigned char digest[MD5_RESULTLEN]; + const char *a1_hex, *a2_hex, *response_hex; + int i; + + /* get the MD5 password */ + if (size != MD5_RESULTLEN) { + e_error(request->auth_request.mech_event, + "invalid credentials length"); + return FALSE; + } + + /* + response = + HEX( KD ( HEX(H(A1)), + { nonce-value, ":" nc-value, ":", + cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) + + and if authzid is not empty: + + A1 = { H( { username-value, ":", realm-value, ":", passwd } ), + ":", nonce-value, ":", cnonce-value, ":", authzid } + + else: + + A1 = { H( { username-value, ":", realm-value, ":", passwd } ), + ":", nonce-value, ":", cnonce-value } + + If the "qop" directive's value is "auth", then A2 is: + + A2 = { "AUTHENTICATE:", digest-uri-value } + + If the "qop" value is "auth-int" or "auth-conf" then A2 is: + + A2 = { "AUTHENTICATE:", digest-uri-value, + ":00000000000000000000000000000000" } + */ + + /* A1 */ + md5_init(&ctx); + md5_update(&ctx, credentials, size); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce, strlen(request->nonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->cnonce, strlen(request->cnonce)); + if (request->authzid != NULL) { + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->authzid, strlen(request->authzid)); + } + md5_final(&ctx, digest); + a1_hex = binary_to_hex(digest, 16); + + /* do it twice, first verify the user's response, the second is + sent for client as a reply */ + for (i = 0; i < 2; i++) { + /* A2 */ + md5_init(&ctx); + if (i == 0) + md5_update(&ctx, "AUTHENTICATE:", 13); + else + md5_update(&ctx, ":", 1); + + if (request->digest_uri != NULL) { + md5_update(&ctx, request->digest_uri, + strlen(request->digest_uri)); + } + if (request->qop == QOP_AUTH_INT || + request->qop == QOP_AUTH_CONF) { + md5_update(&ctx, ":00000000000000000000000000000000", + 33); + } + md5_final(&ctx, digest); + a2_hex = binary_to_hex(digest, 16); + + /* response */ + md5_init(&ctx); + md5_update(&ctx, a1_hex, 32); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce, strlen(request->nonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->nonce_count, + strlen(request->nonce_count)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->cnonce, strlen(request->cnonce)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, request->qop_value, + strlen(request->qop_value)); + md5_update(&ctx, ":", 1); + md5_update(&ctx, a2_hex, 32); + md5_final(&ctx, digest); + response_hex = binary_to_hex(digest, 16); + + if (i == 0) { + /* verify response */ + if (!mem_equals_timing_safe(response_hex, request->response, 32)) { + auth_request_log_info(&request->auth_request, + AUTH_SUBSYS_MECH, + AUTH_LOG_MSG_PASSWORD_MISMATCH); + return FALSE; + } + } else { + request->rspauth = + p_strconcat(request->pool, "rspauth=", + response_hex, NULL); + } + } + + return TRUE; +} + +static bool parse_next(char **data, char **key, char **value) +{ + /* @UNSAFE */ + char *p, *dest; + + p = *data; + while (IS_LWS(*p)) p++; + + /* get key */ + *key = p; + while (*p != '\0' && *p != '=' && *p != ',') + p++; + + if (*p != '=') { + *data = p; + return FALSE; + } + + *value = p+1; + + /* skip trailing whitespace in key */ + while (p > *data && IS_LWS(p[-1])) + p--; + *p = '\0'; + + /* get value */ + p = *value; + while (IS_LWS(*p)) p++; + + if (*p != '"') { + while (*p != '\0' && *p != ',') + p++; + + *data = p; + /* If there is more to parse, ensure it won't get skipped + because *p is set to NUL below */ + if (**data != '\0') (*data)++; + while (IS_LWS(p[-1])) + p--; + *p = '\0'; + } else { + /* quoted string */ + *value = dest = ++p; + while (*p != '\0' && *p != '"') { + if (*p == '\\' && p[1] != '\0') + p++; + *dest++ = *p++; + } + + *data = *p == '"' ? p+1 : p; + *dest = '\0'; + } + + return TRUE; +} + +static bool auth_handle_response(struct digest_auth_request *request, + char *key, char *value, const char **error) +{ + unsigned int i; + + (void)str_lcase(key); + + if (strcmp(key, "realm") == 0) { + if (request->auth_request.fields.realm == NULL && *value != '\0') + auth_request_set_realm(&request->auth_request, value); + return TRUE; + } + + if (strcmp(key, "username") == 0) { + if (request->username != NULL) { + *error = "username must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "empty username"; + return FALSE; + } + + request->username = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "nonce") == 0) { + /* nonce must be same */ + if (strcmp(value, request->nonce) != 0) { + *error = "Invalid nonce"; + return FALSE; + } + + request->nonce_found = TRUE; + return TRUE; + } + + if (strcmp(key, "cnonce") == 0) { + if (request->cnonce != NULL) { + *error = "cnonce must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "cnonce can't contain empty value"; + return FALSE; + } + + request->cnonce = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "nc") == 0) { + unsigned int nc; + + if (request->nonce_count != NULL) { + *error = "nonce-count must not exist more than once"; + return FALSE; + } + + if (str_to_uint(value, &nc) < 0) { + *error = "nonce-count value invalid"; + return FALSE; + } + + if (nc != 1) { + *error = "re-auth not supported currently"; + return FALSE; + } + + request->nonce_count = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "qop") == 0) { + for (i = 0; i < QOP_COUNT; i++) { + if (strcasecmp(qop_names[i], value) == 0) + break; + } + + if (i == QOP_COUNT) { + *error = t_strdup_printf("Unknown QoP value: %s", + str_sanitize(value, 32)); + return FALSE; + } + + request->qop &= (1 << i); + if (request->qop == 0) { + *error = "Nonallowed QoP requested"; + return FALSE; + } + + request->qop_value = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "digest-uri") == 0) { + /* type / host / serv-name */ + const char *const *uri = t_strsplit(value, "/"); + + if (uri[0] == NULL || uri[1] == NULL) { + *error = "Invalid digest-uri"; + return FALSE; + } + + /* FIXME: RFC recommends that we verify the host/serv-type. + But isn't the realm enough already? That'd be just extra + configuration.. Maybe optionally list valid hosts in + config file? */ + request->digest_uri = p_strdup(request->pool, value); + return TRUE; + } + + if (strcmp(key, "maxbuf") == 0) { + if (request->maxbuf != 0) { + *error = "maxbuf must not exist more than once"; + return FALSE; + } + + if (str_to_ulong(value, &request->maxbuf) < 0 || + request->maxbuf == 0) { + *error = "Invalid maxbuf value"; + return FALSE; + } + return TRUE; + } + + if (strcmp(key, "charset") == 0) { + if (strcasecmp(value, "utf-8") != 0) { + *error = "Only utf-8 charset is allowed"; + return FALSE; + } + + return TRUE; + } + + if (strcmp(key, "response") == 0) { + if (strlen(value) != 32) { + *error = "Invalid response value"; + return FALSE; + } + + memcpy(request->response, value, 32); + return TRUE; + } + + if (strcmp(key, "cipher") == 0) { + /* not supported, ignore */ + return TRUE; + } + + if (strcmp(key, "authzid") == 0) { + if (request->authzid != NULL) { + *error = "authzid must not exist more than once"; + return FALSE; + } + + if (*value == '\0') { + *error = "empty authzid"; + return FALSE; + } + + request->authzid = p_strdup(request->pool, value); + return TRUE; + } + + /* unknown key, ignore */ + return TRUE; +} + +static bool parse_digest_response(struct digest_auth_request *request, + const unsigned char *data, size_t size, + const char **error) +{ + char *copy, *key, *value; + bool failed; + + /* + realm="realm" + username="username" + nonce="randomized data" + cnonce="??" + nc=00000001 + qop="auth|auth-int|auth-conf" + digest-uri="serv-type/host[/serv-name]" + response=32 HEX digits + maxbuf=number (with auth-int, auth-conf, defaults to 64k) + charset="utf-8" (iso-8859-1 if it doesn't exist) + cipher="cipher-value" + authzid="authzid-value" + */ + + *error = NULL; + failed = FALSE; + + if (size == 0) { + *error = "Client sent no input"; + return FALSE; + } + + /* treating response as NUL-terminated string also gets rid of all + potential problems with NUL characters in strings. */ + copy = t_strdup_noconst(t_strndup(data, size)); + while (*copy != '\0') { + if (parse_next(©, &key, &value)) { + if (!auth_handle_response(request, key, value, error)) { + failed = TRUE; + break; + } + } + + if (*copy == ',') + copy++; + } + + if (!failed) { + if (!request->nonce_found) { + *error = "Missing nonce parameter"; + failed = TRUE; + } else if (request->cnonce == NULL) { + *error = "Missing cnonce parameter"; + failed = TRUE; + } else if (request->username == NULL) { + *error = "Missing username parameter"; + failed = TRUE; + } + } + + if (request->nonce_count == NULL) + request->nonce_count = p_strdup(request->pool, "00000001"); + if (request->qop_value == NULL) + request->qop_value = p_strdup(request->pool, "auth"); + + return !failed; +} + +static void credentials_callback(enum passdb_result result, + const unsigned char *credentials, size_t size, + struct auth_request *auth_request) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + + switch (result) { + case PASSDB_RESULT_OK: + if (!verify_credentials(request, credentials, size)) { + auth_request_fail(auth_request); + return; + } + + auth_request_success(auth_request, request->rspauth, + strlen(request->rspauth)); + break; + case PASSDB_RESULT_INTERNAL_FAILURE: + auth_request_internal_failure(auth_request); + break; + default: + auth_request_fail(auth_request); + break; + } +} + +static void +mech_digest_md5_auth_continue(struct auth_request *auth_request, + const unsigned char *data, size_t data_size) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + const char *username, *error; + + if (parse_digest_response(request, data, data_size, &error)) { + if (auth_request->fields.realm != NULL && + strchr(request->username, '@') == NULL) { + username = t_strconcat(request->username, "@", + auth_request->fields.realm, NULL); + auth_request->domain_is_realm = TRUE; + } else { + username = request->username; + } + + if (auth_request_set_username(auth_request, username, &error) && + (request->authzid == NULL || + auth_request_set_login_username(auth_request, + request->authzid, + &error))) { + auth_request_lookup_credentials(auth_request, + "DIGEST-MD5", credentials_callback); + return; + } + } + + if (error != NULL) + e_info(auth_request->mech_event, "%s", error); + + auth_request_fail(auth_request); +} + +static void +mech_digest_md5_auth_initial(struct auth_request *auth_request, + const unsigned char *data ATTR_UNUSED, + size_t data_size ATTR_UNUSED) +{ + struct digest_auth_request *request = + (struct digest_auth_request *)auth_request; + string_t *challenge; + + /* FIXME: there's no support for subsequent authentication */ + + challenge = get_digest_challenge(request); + auth_request_handler_reply_continue(auth_request, str_data(challenge), + str_len(challenge)); +} + +static struct auth_request *mech_digest_md5_auth_new(void) +{ + struct digest_auth_request *request; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"digest_md5_auth_request", 2048); + request = p_new(pool, struct digest_auth_request, 1); + request->pool = pool; + request->qop = QOP_AUTH; + + request->auth_request.pool = pool; + return &request->auth_request; +} + +const struct mech_module mech_digest_md5 = { + "DIGEST-MD5", + + .flags = MECH_SEC_DICTIONARY | MECH_SEC_ACTIVE | + MECH_SEC_MUTUAL_AUTH, + .passdb_need = MECH_PASSDB_NEED_LOOKUP_CREDENTIALS, + + mech_digest_md5_auth_new, + mech_digest_md5_auth_initial, + mech_digest_md5_auth_continue, + mech_generic_auth_free +}; |