summaryrefslogtreecommitdiffstats
path: root/src/auth/mech-digest-md5.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/auth/mech-digest-md5.c')
-rw-r--r--src/auth/mech-digest-md5.c592
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(&copy, &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
+};