/* $NetBSD: mech_digestmd5.c,v 1.13 2018/01/30 15:28:39 shm Exp $ */ /* Copyright (c) 2010 The NetBSD Foundation, Inc. * All rights reserved. * * This code is derived from software contributed to The NetBSD Foundation * by Mateusz Kocielski. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by the NetBSD * Foundation, Inc. and its contributors. * 4. Neither the name of The NetBSD Foundation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include __RCSID("$NetBSD: mech_digestmd5.c,v 1.13 2018/01/30 15:28:39 shm Exp $"); #include #include #include #include #include #include #include #include #include #include "buffer.h" #include "crypto.h" #include "error.h" #include "list.h" #include "mech.h" #include "msg.h" #include "saslc_private.h" /* See RFC 2831. */ /* * TODO: * * 1) Add support for Subsequent Authentication (see RFC 2831 section 2.2). */ /* properties */ #define SASLC_DIGESTMD5_AUTHCID SASLC_PROP_AUTHCID #define SASLC_DIGESTMD5_AUTHZID SASLC_PROP_AUTHZID #define SASLC_DIGESTMD5_CIPHERMASK SASLC_PROP_CIPHERMASK #define SASLC_DIGESTMD5_HOSTNAME SASLC_PROP_HOSTNAME #define SASLC_DIGESTMD5_MAXBUF SASLC_PROP_MAXBUF #define SASLC_DIGESTMD5_PASSWD SASLC_PROP_PASSWD #define SASLC_DIGESTMD5_QOPMASK SASLC_PROP_QOPMASK #define SASLC_DIGESTMD5_REALM SASLC_PROP_REALM #define SASLC_DIGESTMD5_SERVICE SASLC_PROP_SERVICE #define SASLC_DIGESTMD5_SERVNAME SASLC_PROP_SERVNAME /* * XXX: define this if you want to be able to set a fixed cnonce for * debugging purposes. */ #define SASLC_DIGESTMD5_CNONCE "CNONCE" /* * XXX: define this if you want to test the saslc_sess_encode() and * saslc_sess_decode() routines against themselves, i.e., have them * use the same key. */ #define SASLC_DIGESTMD5_SELFTEST "SELFTEST" #define DEFAULT_QOP_MASK (F_QOP_NONE | F_QOP_INT | F_QOP_CONF) #define DEFAULT_CIPHER_MASK (F_CIPHER_DES | F_CIPHER_3DES | \ F_CIPHER_RC4 | F_CIPHER_RC4_40 | \ F_CIPHER_RC4_56 | F_CIPHER_AES) #define DEFAULT_MAXBUF 0x10000 #define MAX_MAXBUF 0xffffff #define INVALID_MAXBUF(m) ((m) <= sizeof(md5hash_t) && (m) > MAX_MAXBUF) #define NONCE_LEN 33 /* Minimum recommended length is 64bits (rfc2831). cyrus-sasl uses 33 bytes. */ typedef enum { CHALLENGE_IGNORE = -1, /* must be -1 */ CHALLENGE_REALM = 0, CHALLENGE_NONCE = 1, CHALLENGE_QOP = 2, CHALLENGE_STALE = 3, CHALLENGE_MAXBUF = 4, CHALLENGE_CHARSET = 5, CHALLENGE_ALGORITHM = 6, CHALLENGE_CIPHER = 7 } challenge_t; typedef enum { /* * NB: Values used to index cipher_tbl[] and cipher_ctx_tbl[] * in cipher_context_create(). */ CIPHER_DES = 0, CIPHER_3DES = 1, CIPHER_RC4 = 2, CIPHER_RC4_40 = 3, CIPHER_RC4_56 = 4, CIPHER_AES = 5 } cipher_t; #define F_CIPHER_DES (1 << CIPHER_DES) #define F_CIPHER_3DES (1 << CIPHER_3DES) #define F_CIPHER_RC4 (1 << CIPHER_RC4) #define F_CIPHER_RC4_40 (1 << CIPHER_RC4_40) #define F_CIPHER_RC4_56 (1 << CIPHER_RC4_56) #define F_CIPHER_AES (1 << CIPHER_AES) static const named_flag_t cipher_tbl[] = { /* NB: to be indexed by cipher_t values */ { "des", F_CIPHER_DES }, { "3des", F_CIPHER_3DES }, { "rc4", F_CIPHER_RC4 }, { "rc4-40", F_CIPHER_RC4_40 }, { "rc4-56", F_CIPHER_RC4_56 }, { "aes", F_CIPHER_AES }, { NULL, 0 } }; static inline const char * cipher_name(cipher_t cipher) { assert(cipher < __arraycount(cipher_tbl) - 1); /* NULL terminated */ if (cipher < __arraycount(cipher_tbl) - 1) return cipher_tbl[cipher].name; return NULL; } static inline unsigned int cipher_list_flags(list_t *list) { return saslc__list_flags(list, cipher_tbl); } typedef struct { /* data parsed from challenge */ bool utf8; bool algorithm; bool stale; char * nonce; list_t * realm; uint32_t cipher_flags; uint32_t qop_flags; size_t maxbuf; } cdata_t; typedef struct { /* response data */ /* NB: the qop is in saslc__mech_sess_t */ char *authcid; char *authzid; char *cnonce; char *digesturi; char *passwd; char *realm; cipher_t cipher; int nonce_cnt; size_t maxbuf; } rdata_t; typedef uint8_t md5hash_t[MD5_DIGEST_LENGTH]; typedef struct { md5hash_t kic; /* client->server integrity key */ md5hash_t kis; /* server->client integrity key */ md5hash_t kcc; /* client->server confidentiality key */ md5hash_t kcs; /* server->client confidentiality key */ } keys_t; typedef struct cipher_context_t { size_t blksize; /* block size for cipher */ EVP_CIPHER_CTX *evp_ctx; /* openssl EVP context */ } cipher_context_t; typedef struct coder_context_t { uint8_t *key; /* key for coding */ uint32_t seqnum; /* 4 byte sequence number */ void *buf_ctx; /* buffer context */ cipher_context_t *cph_ctx; /* cipher context */ saslc_sess_t *sess; /* session: for error setting */ } coder_context_t; /* mech state */ typedef struct { saslc__mech_sess_t mech_sess; /* must be first */ cdata_t cdata; /* data parsed from challenge string */ rdata_t rdata; /* data used for response string */ keys_t keys; /* keys */ coder_context_t dec_ctx; /* decode context */ coder_context_t enc_ctx; /* encode context */ } saslc__mech_digestmd5_sess_t; /** * @brief if possible convert a UTF-8 string to a ISO8859-1 string. * @param utf8 original UTF-8 string. * @param iso8859 pointer to pointer to the malloced ISO8859-1 string. * @return -1 if the string cannot be translated. * * NOTE: this allocates memory for its output and the caller is * responsible for freeing it. */ static int utf8_to_8859_1(char *utf8, char **iso8859) { unsigned char *s, *d, *end, *src; size_t cnt; src = (unsigned char *)utf8; cnt = 0; end = src + strlen(utf8); for (s = src; s < end; ++s) { if (*s > 0xC3) /* abort if outside 8859-1 range */ return -1; /* * Look for valid 2 byte UTF-8 encoding with, 8 bits * of info. Quit if invalid pair found. */ if (*s >= 0xC0 && *s <= 0xC3) { /* 2 bytes, 8 bits */ if (++s == end || *s < 0x80 || *s > 0xBF) return -1; /* broken utf-8 encoding */ } cnt++; } /* Allocate adequate space. */ d = malloc(cnt + 1); if (d == NULL) return -1; *iso8859 = (char *)d; /* convert to 8859-1 */ do { for (s = src; s < end && *s < 0xC0; ++s) *d++ = *s; if (s + 1 >= end) break; *d++ = ((s[0] & 0x3) << 6) | (s[1] & 0x3f); src = s + 2; } while (src < end); *d = '\0'; return 0; } /** * @brief unquote a string by removing escapes. * @param str string to unquote. * @return NULL on failure * * NOTE: this allocates memory for its output and the caller is * responsible for freeing it. */ static char * unq(const char *str) { const char *s; char *unq_str, *d; int escaped; unq_str = malloc(strlen(str) + 1); if (unq_str == NULL) return NULL; escaped = 0; d = unq_str; for (s = str; *s != '\0'; s++) { switch (*s) { case '\\': if (escaped) *d++ = *s; escaped = !escaped; break; default: *d++ = *s; escaped = 0; } } *d = '\0'; return unq_str; } /** * @brief computing MD5(username:realm:password). * @param ms mechanism session * @param buf buffer for hash * @return 0 on success, -1 on failure */ static int saslc__mech_digestmd5_userhash(saslc__mech_digestmd5_sess_t *ms, uint8_t *buf) { char *tmp; char *unq_username, *unq_realm; ssize_t len; if ((unq_username = unq(ms->rdata.authcid)) == NULL) return -1; /********************************************************/ /* RFC 2831 section 2.1.2 */ /* ... If the directive is missing, "realm-value" will */ /* set to the empty string when computing A1. */ /********************************************************/ if (ms->rdata.realm == NULL) unq_realm = strdup(""); else unq_realm = unq(ms->rdata.realm); if (unq_realm == NULL) { free(unq_username); return -1; } len = asprintf(&tmp, "%s:%s:%s", unq_username, unq_realm, ms->rdata.passwd); free(unq_realm); free(unq_username); if (len == -1) return -1; saslc__crypto_md5_hash(tmp, (size_t)len, buf); memset(tmp, 0, (size_t)len); free(tmp); return 0; } /** * @brief setup the appropriate QOP keys as determined by the chosen * QOP type (see RFC2831 sections 2.3 and 2.4). * @param ms mechanism session * @param a1hash MD5(a1) * @return 0 on success, -1 on failure */ static int setup_qop_keys(saslc__mech_digestmd5_sess_t *ms, md5hash_t a1hash) { #define KIC_MAGIC "Digest session key to client-to-server signing key magic constant" #define KIS_MAGIC "Digest session key to server-to-client signing key magic constant" #define KCC_MAGIC "Digest H(A1) to client-to-server sealing key magic constant" #define KCS_MAGIC "Digest H(A1) to server-to-client sealing key magic constant" #define KIC_MAGIC_LEN (sizeof(KIC_MAGIC) - 1) #define KIS_MAGIC_LEN (sizeof(KIS_MAGIC) - 1) #define KCC_MAGIC_LEN (sizeof(KCC_MAGIC) - 1) #define KCS_MAGIC_LEN (sizeof(KCS_MAGIC) - 1) #define MAX_MAGIC_LEN KIC_MAGIC_LEN char buf[MD5_DIGEST_LENGTH + MAX_MAGIC_LEN]; size_t buflen; size_t n; switch (ms->mech_sess.qop) { case QOP_NONE: /* nothing to do */ break; case QOP_CONF: /*************************************************************************/ /* See RFC2831 section 2.4 (Confidentiality Protection) */ /* */ /* The key for confidentiality protecting messages from client to server */ /* is: */ /* */ /* Kcc = MD5({H(A1)[0..n], */ /* "Digest H(A1) to client-to-server sealing key magic constant"}) */ /* */ /* The key for confidentiality protecting messages from server to client */ /* is: */ /* */ /* Kcs = MD5({H(A1)[0..n], */ /* "Digest H(A1) to server-to-client sealing key magic constant"}) */ /* */ /* where MD5 is as specified in [RFC 1321]. For cipher "rc4-40" n is 5; */ /* for "rc4-56" n is 7; for the rest n is 16. */ /*************************************************************************/ switch (ms->rdata.cipher) { case CIPHER_RC4_40: n = 5; break; case CIPHER_RC4_56: n = 7; break; default: n = MD5_DIGEST_LENGTH; break; } memcpy(buf, a1hash, n); memcpy(buf + n, KCC_MAGIC, KCC_MAGIC_LEN); buflen = n + KCC_MAGIC_LEN; saslc__crypto_md5_hash(buf, buflen, ms->keys.kcc); memcpy(buf + n, KCS_MAGIC, KCS_MAGIC_LEN); buflen = n + KCS_MAGIC_LEN; saslc__crypto_md5_hash(buf, buflen, ms->keys.kcs); /*FALLTHROUGH*/ case QOP_INT: /*************************************************************************/ /* See RFC2831 section 2.3 (Integrity Protection) */ /* The key for integrity protecting messages from client to server is: */ /* */ /* Kic = MD5({H(A1), */ /* "Digest session key to client-to-server signing key magic constant"}) */ /* */ /* The key for integrity protecting messages from server to client is: */ /* */ /* Kis = MD5({H(A1), */ /* "Digest session key to server-to-client signing key magic constant"}) */ /*************************************************************************/ memcpy(buf, a1hash, MD5_DIGEST_LENGTH); memcpy(buf + MD5_DIGEST_LENGTH, KIC_MAGIC, KIC_MAGIC_LEN); buflen = MD5_DIGEST_LENGTH + KIC_MAGIC_LEN; saslc__crypto_md5_hash(buf, buflen, ms->keys.kic); memcpy(buf + MD5_DIGEST_LENGTH, KIS_MAGIC, KIS_MAGIC_LEN); buflen = MD5_DIGEST_LENGTH + KIS_MAGIC_LEN; saslc__crypto_md5_hash(buf, buflen, ms->keys.kis); break; } return 0; #undef KIC_MAGIC #undef KIS_MAGIC #undef KCC_MAGIC #undef KCS_MAGIC #undef KIC_MAGIC_LEN #undef KIS_MAGIC_LEN #undef KCC_MAGIC_LEN #undef KCS_MAGIC_LEN #undef MAX_MAGIC_LEN } /** * @brief computes A1 hash value (see: RFC2831) * @param ms mechanism session * @return hash in hex form */ static char * saslc__mech_digestmd5_a1(saslc__mech_digestmd5_sess_t *ms) { char *tmp1, *tmp2, *r; char *unq_authzid; md5hash_t a1hash, userhash; int plen; size_t len; /*****************************************************************************/ /* If authzid is specified, then A1 is */ /* */ /* A1 = { H({ unq(username-value), ":", unq(realm-value), ":", passwd }), */ /* ":", nonce-value, ":", cnonce-value, ":", unq(authzid-value) } */ /* */ /* If authzid is not specified, then A1 is */ /* */ /* A1 = { H({ unq(username-value), ":", unq(realm-value), ":", passwd }), */ /* ":", nonce-value, ":", cnonce-value } */ /*****************************************************************************/ if (saslc__mech_digestmd5_userhash(ms, userhash) == -1) return NULL; if (ms->rdata.authzid == NULL) plen = asprintf(&tmp1, ":%s:%s", ms->cdata.nonce, ms->rdata.cnonce); else { if ((unq_authzid = unq(ms->rdata.authzid)) == NULL) return NULL; plen = asprintf(&tmp1, ":%s:%s:%s", ms->cdata.nonce, ms->rdata.cnonce, unq_authzid); free(unq_authzid); } if (plen == -1) return NULL; len = plen; tmp2 = malloc(MD5_DIGEST_LENGTH + len); if (tmp2 == NULL) { free(tmp1); return NULL; } memcpy(tmp2, userhash, MD5_DIGEST_LENGTH); memcpy(tmp2 + MD5_DIGEST_LENGTH, tmp1, len); free(tmp1); saslc__crypto_md5_hash(tmp2, MD5_DIGEST_LENGTH + len, a1hash); free(tmp2); r = saslc__crypto_hash_to_hex(a1hash); setup_qop_keys(ms, a1hash); return r; } /** * @brief computes A2 hash value (see: RFC2831) * @param ms mechanism session * @param method string indicating method "AUTHENTICATE" or "" * @return hash converted to ascii */ static char * saslc__mech_digestmd5_a2(saslc__mech_digestmd5_sess_t *ms, const char *method) { char *tmp, *r; int rval; /*****************************************************************/ /* 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" } */ /*****************************************************************/ rval = -1; switch(ms->mech_sess.qop) { case QOP_NONE: rval = asprintf(&tmp, "%s:%s", method, ms->rdata.digesturi); break; case QOP_INT: case QOP_CONF: rval = asprintf(&tmp, "%s:%s:00000000000000000000000000000000", method, ms->rdata.digesturi); break; } if (rval == -1) return NULL; r = saslc__crypto_md5_hex(tmp, strlen(tmp)); free(tmp); return r; } /** * @brief computes result hash. * @param ms mechanism session * @param a1 A1 hash value * @param a2 A2 hash value * @return hash converted to ascii, NULL on failure. */ static char * saslc__mech_digestmd5_rhash(saslc__mech_digestmd5_sess_t *ms, const char *a1, const char *a2) { char *tmp, *r; /******************************************************************/ /* response-value = */ /* HEX( KD ( HEX(H(A1)), */ /* { nonce-value, ":" nc-value, ":", */ /* cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) */ /******************************************************************/ if (asprintf(&tmp, "%s:%s:%08x:%s:%s:%s", a1, ms->cdata.nonce, ms->rdata.nonce_cnt, ms->rdata.cnonce, saslc__mech_qop_name(ms->mech_sess.qop), a2) == -1) return NULL; r = saslc__crypto_md5_hex(tmp, strlen(tmp)); free(tmp); return r; } /** * @brief building response string. Basing on * session and mechanism properties. * @param ms mechanism session * @param method string indicating method: "AUTHENTICATE" or "" * @return response string, NULL on failure. */ static char * saslc__mech_digestmd5_response(saslc__mech_digestmd5_sess_t *ms, const char *method) { char *r, *a1, *a2; /******************************************************************/ /* charset = "charset" "=" "utf-8" */ /* */ /* This directive, if present, specifies that the client has used */ /* UTF-8 [UTF-8] encoding for the username, realm and */ /* password. If present, the username, realm and password are in */ /* Unicode, prepared using the "SASLPrep" profile [SASLPrep] of */ /* the "stringprep" algorithm [StringPrep] and than encoded as */ /* UTF-8 [UTF-8]. If not present, the username and password must */ /* be encoded in ISO 8859-1 [ISO-8859] (of which US-ASCII */ /* [USASCII] is a subset). The client should send this directive */ /* only if the server has indicated it supports UTF-8 */ /* [UTF-8]. The directive is needed for backwards compatibility */ /* with HTTP Digest, which only supports ISO 8859-1. */ /******************************************************************/ /* * NOTE: We don't set charset in the response, so this is not * an issue here. However, see the note in stringprep_realms() * which is called when processing the challenge. */ /******************************************************************/ /* response-value = */ /* HEX( KD ( HEX(H(A1)), */ /* { nonce-value, ":" nc-value, ":", */ /* cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) */ /******************************************************************/ r = NULL; a1 = saslc__mech_digestmd5_a1(ms); if (a1 == NULL) return NULL; a2 = saslc__mech_digestmd5_a2(ms, method); if (a2 != NULL) { r = saslc__mech_digestmd5_rhash(ms, a1, a2); free(a2); } free(a1); return r; } /** * @brief Choose a string from a user provided host qualified list, * i.e., a comma delimited list with possible hostname qualifiers on * the elements. * @param hqlist a comma delimited list with entries of the form * "[hostname:]string". * @param hostname the hostname to use in the selection. * @param rval pointer to location for returned string. Set to NULL * if none found, otherwise set to strdup(3) of the string found. * @return 0 on success, -1 on failure (no memory). * * NOTE: hqlist and rval must not be NULL. * NOTE: this allocates memory for its output and the caller is * responsible for freeing it. */ static int choose_from_hqlist(const char *hqlist, const char *hostname, char **rval) { list_t *l, *list; size_t len; char *p; if (saslc__list_parse(&list, hqlist) == -1) return -1; /* no memory */ /* * If the user provided a list and the caller provided a * hostname, pick the first string from the list that * corresponds to the hostname. */ if (hostname != NULL) { len = strlen(hostname); for (l = list; l != NULL; l = l->next) { p = l->value + len; if (*p != ':' || strncasecmp(l->value, hostname, len) != 0) continue; if (*(++p) != '\0' && isalnum((unsigned char)*p)) { if ((p = strdup(p)) == NULL) goto nomem; goto done; } } } /* * If one couldn't be found, look for first string in the list * without a hostname specifier. */ p = NULL; for (l = list; l != NULL; l = l->next) { if (strchr(l->value, ':') == NULL) { if ((p = strdup(l->value)) == NULL) goto nomem; goto done; } } done: saslc__list_free(list); *rval = p; return 0; nomem: saslc__list_free(list); return -1; } /** * @brief builds digesturi string * @param serv_type type of service to use, e.g., "smtp" * @param host fully-qualified canonical DNS name of host * @param serv_name service name if it is replicated via DNS records; may * be NULL. * @return digesturi string, NULL on failure. */ static char * saslc__mech_digestmd5_digesturi(saslc_sess_t *sess, const char *serv_host) { const char *serv_list; char *serv_name; const char *serv_type; char *r; int rv; serv_type = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SERVICE); if (serv_type == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "service is required for an authentication"); return NULL; } serv_list = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SERVNAME); if (serv_list == NULL) serv_name = NULL; else if (choose_from_hqlist(serv_list, serv_host, &serv_name) == -1) goto nomem; saslc__msg_dbg("%s: serv_name='%s'", __func__, serv_name ? serv_name : ""); /****************************************************************/ /* digest-uri = "digest-uri" "=" <"> digest-uri-value <"> */ /* digest-uri-value = serv-type "/" host [ "/" serv-name ] */ /* */ /* If the service is not replicated, or the serv-name is */ /* identical to the host, then the serv-name component MUST be */ /* omitted. The service is considered to be replicated if the */ /* client's service-location process involves resolution using */ /* standard DNS lookup operations, and if these operations */ /* involve DNS records (such as SRV, or MX) which resolve one */ /* DNS name into a set of other DNS names. */ /****************************************************************/ rv = serv_name == NULL || strcmp(serv_host, serv_name) == 0 ? asprintf(&r, "%s/%s", serv_type, serv_host) : asprintf(&r, "%s/%s/%s", serv_type, serv_host, serv_name); if (serv_name != NULL) free(serv_name); if (rv == -1) goto nomem; saslc__msg_dbg("%s: digest-uri='%s'", __func__, r); return r; nomem: saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return NULL; } /** * @brief creates client's nonce. (Basing on crypto.h) * @param s length of nonce * @return nonce string, NULL on failure. */ static char * saslc__mech_digestmd5_nonce(size_t s) { char *nonce; char *r; nonce = saslc__crypto_nonce(s); if (nonce == NULL) return NULL; if (saslc__crypto_encode_base64(nonce, s, &r, NULL) == -1) return NULL; free(nonce); return r; } /** * @brief strip quotes from a string (modifies the string) * @param str the string * @return string without quotes. */ static char * strip_quotes(char *str) { char *p; size_t len; if (*str != '"') return str; len = strlen(str); p = str + len; if (len < 2 || p[-1] != '"') return str; p[-1] = '\0'; return ++str; } /** * @brief convert a list of realms from utf-8 to iso8859-q if necessary. * @param is_utf8 the characterset of the realms (true if utf8) * @param realms the realm list */ static int stringprep_realms(bool is_utf8, list_t *realms) { list_t *l; char *utf8, *iso8859; /******************************************************************/ /* If at least one realm is present and the charset directive is */ /* also specified (which means that realm(s) are encoded as */ /* UTF-8), the client should prepare each instance of realm using */ /* the "SASLPrep" profile [SASLPrep] of the "stringprep" */ /* algorithm [StringPrep]. If preparation of a realm instance */ /* fails or results in an empty string, the client should abort */ /* the authentication exchange. */ /******************************************************************/ if (!is_utf8) return 0; for (l = realms; l != NULL; l = l->next) { utf8 = l->value; if (utf8_to_8859_1(utf8, &iso8859) == -1) return -1; free(utf8); l->value = iso8859; } return 0; } /** * @brief choose a realm from a list of possible realms provided by the server * @param sess the session context * @param realms the list of realms * @return our choice of realm or NULL on failure. It is the user's * responsibility to free the memory allocated for the return string. */ static char * choose_realm(saslc_sess_t *sess, const char *hostname, list_t *realms) { const char *user_realms; list_t *l; char *p = NULL; /*****************************************************************/ /* The realm containing the user's account. This directive is */ /* required if the server provided any realms in the */ /* "digest-challenge", in which case it may appear exactly once */ /* and its value SHOULD be one of those realms. If the directive */ /* is missing, "realm-value" will set to the empty string when */ /* computing A1 (see below for details). */ /*****************************************************************/ user_realms = saslc_sess_getprop(sess, SASLC_DIGESTMD5_REALM); /* * If the challenge provided no realms, try to pick one from a * user specified list, which may be keyed by the hostname. * If one can't be found, return NULL; */ if (realms == NULL) { /* * No realm was supplied in challenge. Figure out a * plausable default. */ if (user_realms == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "cannot determine the realm"); return NULL; } if (choose_from_hqlist(user_realms, hostname, &p) == -1) goto nomem; if (p == NULL) saslc__error_set(ERR(sess), ERROR_MECH, "cannot choose a realm"); return p; } /************************************************************/ /* Multiple realm directives are allowed, in which case the */ /* user or client must choose one as the realm for which to */ /* supply to username and password. */ /************************************************************/ /* * If the user hasn't specified any realms, or we can't find * one from the user provided list, just take the first realm * from the challenge. */ if (user_realms == NULL) goto use_1st_realm; if (choose_from_hqlist(user_realms, hostname, &p) == -1) goto nomem; if (p == NULL) goto use_1st_realm; /* * If we found a matching user provide realm, make sure it is * on the list of realms. If it isn't, just take the first * realm in the challenge. */ for (l = realms; l != NULL; l = l->next) { if (strcasecmp(p, l->value) == 0) return p; } use_1st_realm: free(p); if ((p = strdup(realms->value)) == NULL) goto nomem; return p; nomem: saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return NULL; } /** * @brief destroy a cipher context * @param ctx cipher context * @return nothing */ static void cipher_context_destroy(cipher_context_t *ctx) { if (ctx != NULL) { if (ctx->evp_ctx != NULL) EVP_CIPHER_CTX_free(ctx->evp_ctx); free(ctx); } } /** * @brief slide the bits from 7 bytes into the high 7 bits of 8 bites * @param ikey input key * @param okey output key * * This matches cyrus-sasl 2.1.23 */ static inline void slidebits(uint8_t *ikey, uint8_t *okey) { okey[0] = ikey[0] << 0; okey[1] = ikey[0] << 7 | (unsigned)ikey[1] >> 1; okey[2] = ikey[1] << 6 | (unsigned)ikey[2] >> 2; okey[3] = ikey[2] << 5 | (unsigned)ikey[3] >> 3; okey[4] = ikey[3] << 4 | (unsigned)ikey[4] >> 4; okey[5] = ikey[4] << 3 | (unsigned)ikey[5] >> 5; okey[6] = ikey[5] << 2 | (unsigned)ikey[6] >> 6; okey[7] = ikey[6] << 1; } /** * @brief convert our key to a DES key * @param key our key * @param keylen our key length * @param deskey the key in DES format * * NOTE: The openssl implementations of "des" and "3des" expect their * keys to be in the high 7 bits of 8 bytes and 16 bytes, * respectively. Thus, our key length will be 7 and 14 bytes, * respectively. */ static void make_deskey(uint8_t *key, size_t keylen, uint8_t *deskey) { assert(keylen == 7 || keylen == 14); slidebits(deskey + 0, key + 0); if (keylen == 14) slidebits(deskey + 7, key + 7); } /** * @brief create a cipher context, including EVP cipher initialization. * @param sess session context * @param cipher cipher to use * @param do_enc encode context if set, decode context if 0 * @param key crypt key to use * @return cipher context, or NULL on error */ static cipher_context_t * cipher_context_create(saslc_sess_t *sess, cipher_t cipher, int do_enc, uint8_t *key) { #define AES_IV_MAGIC "aes-128" #define AES_IV_MAGIC_LEN (sizeof(AES_IV_MAGIC) - 1) static const struct cipher_ctx_tbl_s { cipher_t eval; /* for error checking */ const EVP_CIPHER *(*evp_type)(void);/* type of cipher */ size_t keylen; /* key length */ ssize_t blksize; /* block size for cipher */ size_t ivlen; /* initial value length */ } cipher_ctx_tbl[] = { /* NB: table indexed by cipher_t */ /* eval evp_type keylen blksize ivlen */ { CIPHER_DES, EVP_des_cbc, 7, 8, 8 }, { CIPHER_3DES, EVP_des_ede_cbc, 14, 8, 8 }, { CIPHER_RC4, EVP_rc4, 16, 1, 0 }, { CIPHER_RC4_40, EVP_rc4, 5, 1, 0 }, { CIPHER_RC4_56, EVP_rc4, 7, 1, 0 }, { CIPHER_AES, EVP_aes_128_cbc, 16, 16, 16 } }; const struct cipher_ctx_tbl_s *ctp; char buf[sizeof(md5hash_t) + AES_IV_MAGIC_LEN]; uint8_t deskey[16]; md5hash_t aes_iv; /* initial value buffer for aes */ cipher_context_t *ctx; /* cipher context */ uint8_t *ivp; const char *errmsg; int rv; /*************************************************************************/ /* See draft-ietf-sasl-rfc2831bis-02.txt section 2.4 (mentions "aes") */ /* The key for the "rc4" and "aes" ciphers is all 16 bytes of Kcc or Kcs.*/ /* The key for the "rc4-40" cipher is the first 5 bytes of Kcc or Kcs. */ /* The key for the "rc4-56" is the first 7 bytes of Kcc or Kcs. */ /* The key for "des" is the first 7 bytes of Kcc or Kcs. */ /* The key for "3des" is the first 14 bytes of Kcc or Kcs. */ /* */ /* The IV used to send/receive the initial buffer of security encoded */ /* data for "des" and "3des" is the last 8 bytes of Kcc or Kcs. For all */ /* subsequent buffers the last 8 bytes of the ciphertext of the buffer */ /* NNN is used as the IV for the buffer (NNN + 1). */ /* */ /* The IV for the "aes" cipher in CBC mode for messages going from the */ /* client to the server (IVc) consists of 16 bytes calculated as */ /* follows: IVc = MD5({Kcc, "aes-128"}) */ /* */ /* The IV for the "aes" cipher in CBC mode for messages going from the */ /* server to the client (IVs) consists of 16 bytes calculated as */ /* follows: IVs = MD5({Kcs, "aes-128"}) */ /*************************************************************************/ assert(cipher < __arraycount(cipher_ctx_tbl)); if (cipher >= __arraycount(cipher_ctx_tbl)) { saslc__error_set_errno(ERR(sess), ERROR_BADARG); return NULL; } ctx = malloc(sizeof(*ctx)); if (ctx == NULL) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return NULL; } ctp = &cipher_ctx_tbl[cipher]; assert(ctp->eval == cipher); ctx->blksize = ctp->blksize; ctx->evp_ctx = EVP_CIPHER_CTX_new(); if (ctx->evp_ctx == NULL) { errmsg = "EVP_CIPHER_CTX_new failed"; goto err; } if (EVP_CipherInit_ex(ctx->evp_ctx, ctp->evp_type(), NULL, NULL, NULL, do_enc) == 0) { errmsg = "EVP_CipherInit_ex failed"; goto err; } if (EVP_CIPHER_CTX_set_padding(ctx->evp_ctx, 0) == 0) { errmsg = "EVP_CIPHER_CTX_set_padding failed"; goto err; } ivp = NULL; switch (cipher) { /* prepare key and IV */ case CIPHER_RC4: case CIPHER_RC4_40: case CIPHER_RC4_56: assert(ctp->ivlen == 0); /* no IV */ rv = EVP_CIPHER_CTX_set_key_length(ctx->evp_ctx, (int)ctp->keylen); if (rv == 0) { errmsg = "EVP_CIPHER_CTX_set_key_length failed"; goto err; } break; case CIPHER_DES: case CIPHER_3DES: assert(ctp->ivlen == 8); ivp = key + 8; make_deskey(key, ctp->keylen, deskey); key = deskey; break; case CIPHER_AES: assert(ctp->ivlen == 16); /* IVs = MD5({Kcs, "aes-128"}) */ memcpy(buf, key, sizeof(md5hash_t)); memcpy(buf + sizeof(md5hash_t), AES_IV_MAGIC, AES_IV_MAGIC_LEN); saslc__crypto_md5_hash(buf, sizeof(buf), aes_iv); ivp = aes_iv; break; } if (EVP_CipherInit_ex(ctx->evp_ctx, NULL, NULL, key, ivp, do_enc) == 0) { errmsg = "EVP_CipherInit_ex 2 failed"; goto err; } return ctx; err: cipher_context_destroy(ctx); saslc__error_set(ERR(sess), ERROR_MECH, errmsg); return NULL; #undef AES_IV_MAGIC_LEN #undef AES_IV_MAGIC } /** * @brief compute the necessary padding length * @param ctx the cipher context * @param inlen the data length to put in the packet * @return the length of padding needed (zero if none needed) */ static size_t get_padlen(cipher_context_t *ctx, size_t inlen) { size_t blksize; if (ctx == NULL) return 0; blksize = ctx->blksize; if (blksize == 1) return 0; return blksize - ((inlen + 10) % blksize); } /** * @brief compute the packet integrity including the version and * sequence number * @param key the hmac_md5 hash key * @param seqnum the sequence number * @param in the input buffer * @param inlen the input buffer length * @return 0 on success, -1 on failure */ static int packet_integrity(md5hash_t key, uint32_t seqnum, void *in, size_t inlen, md5hash_t mac) { be32enc(in, seqnum); if (saslc__crypto_hmac_md5_hash(key, MD5_DIGEST_LENGTH, in, inlen, mac) == -1) return -1; /* we keep only the first 10 bytes of the hash */ be16enc(mac + 10, 0x0001); /* add 2 byte version number */ be32enc(mac + 12, seqnum); /* add 4 byte sequence number */ return 0; } /** * @brief encode or decode a buffer (in place) * @param ctx the cipher context * @param in the input buffer * @param inlen the buffer length * @return the length of the result left in the input buffer after * processing, or -1 on failure. */ static ssize_t cipher_update(cipher_context_t *ctx, void *in, size_t inlen) { int outl, rv; void *out; out = in; /* XXX: this assumes we can encoded and decode in place */ rv = EVP_CipherUpdate(ctx->evp_ctx, out, &outl, in, (int)inlen); if (rv == 0) return -1; return outl; } /** * @brief incapsulate a message with confidentiality (sign and encrypt) * @param ctx coder context * @param in pointer to message to encode * @param inlen length of message * @param out encoded output packet (including prefixed 4 byte length field) * @param outlen decoded output packet length * @returns 0 on success, -1 on failure * * NOTE: this allocates memory for its output and the caller is * responsible for freeing it. * * integrity (auth-int): * len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum * * confidentiality (auth-conf): * len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum */ static ssize_t encode_buffer(coder_context_t *ctx, const void *in, size_t inlen, void **out, size_t *outlen) { void *buf; uint8_t *mac, *p; ssize_t tmplen; size_t buflen; size_t padlen; padlen = get_padlen(ctx->cph_ctx, inlen); buflen = 4 + inlen + padlen + sizeof(md5hash_t); buf = malloc(buflen); if (buf == NULL) { saslc__error_set_errno(ERR(ctx->sess), ERROR_NOMEM); return -1; } p = buf; memcpy(p + 4, in, inlen); mac = p + 4 + inlen + padlen; if (packet_integrity(ctx->key, ctx->seqnum, buf, 4 + inlen, mac) == -1) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "HMAC failed"); free(buf); return -1; } if (padlen) memset(p + 4 + inlen, (int)padlen, padlen); if (ctx->cph_ctx != NULL) { if ((tmplen = cipher_update(ctx->cph_ctx, p + 4, inlen + padlen + 10)) == -1) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "cipher error"); free(buf); return -1; } assert((size_t)tmplen == inlen + padlen + 10); if ((size_t)tmplen != inlen + padlen + 10) return -1; } be32enc(buf, (uint32_t)(buflen - 4)); *out = buf; *outlen = buflen; ctx->seqnum++; /* wraps at 2^32 */ return 0; } /** * @brief decode one complete confidentiality encoded packet * @param ctx coder context * @param in pointer to packet, including the beginning 4 byte length field. * @param inlen length of packet * @param out decoded output * @param outlen decoded output length * @returns 0 on success, -1 on failure * * NOTE: this modifies the intput buffer! * NOTE: this allocates memory for its output and the caller is * responsible for freeing it. * * integrity (auth-int): * len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum * * confidentiality (auth-conf): * len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum */ static ssize_t decode_buffer(coder_context_t *ctx, void *in, size_t inlen, void **out, size_t *outlen) { md5hash_t mac; void *buf; uint8_t *p; size_t blksize, buflen, padlen; ssize_t tmplen; uint32_t len; padlen = get_padlen(ctx->cph_ctx, 1); if (inlen < 4 + 1 + padlen + MD5_DIGEST_LENGTH) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "zero payload packet"); return -1; } len = be32dec(in); if (len + 4 != inlen) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "bad packet length"); return -1; } if (ctx->cph_ctx != NULL) { p = in; if ((tmplen = cipher_update(ctx->cph_ctx, p + 4, len - 6)) == -1) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "cipher error"); return -1; } assert(tmplen == (ssize_t)len - 6); if (tmplen != (ssize_t)len - 6) return -1; } blksize = ctx->cph_ctx ? ctx->cph_ctx->blksize : 0; if (blksize <= 1) padlen = 0; else{ p = in; padlen = p[inlen - sizeof(md5hash_t) - 1]; if (padlen > blksize || padlen == 0) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "invalid padding length after decode"); return -1; } } if (packet_integrity(ctx->key, ctx->seqnum, in, inlen - padlen - sizeof(mac), mac) == -1) { saslc__error_set(ERR(ctx->sess), ERROR_MECH, "HMAC failed"); return -1; } p = in; p += 4 + len - MD5_DIGEST_LENGTH; if (memcmp(p, mac, MD5_DIGEST_LENGTH) != 0) { uint32_t seqnum; p = in; seqnum = be32dec(p + inlen - 4); saslc__error_set(ERR(ctx->sess), ERROR_MECH, seqnum != ctx->seqnum ? "invalid MAC (bad seqnum)" : "invalid MAC"); return -1; } buflen = len - padlen - MD5_DIGEST_LENGTH; buf = malloc(buflen); if (buf == NULL) { saslc__error_set_errno(ERR(ctx->sess), ERROR_NOMEM); return -1; } p = in; p += 4; memcpy(buf, p, buflen); *out = buf; *outlen = buflen; ctx->seqnum++; return 0; } /** * @brief add integrity or confidentiality layer * @param sess session handle * @param in input buffer * @param inlen input buffer length * @param out pointer to output buffer * @param out pointer to output buffer length * @return number of bytes consumed on success, 0 if insufficient data * to process, -1 on failure */ static ssize_t saslc__mech_digestmd5_encode(saslc_sess_t *sess, const void *in, size_t inlen, void **out, size_t *outlen) { saslc__mech_digestmd5_sess_t *ms; uint8_t *buf; size_t buflen; ssize_t rval; ms = sess->mech_sess; assert(ms->mech_sess.qop != QOP_NONE); if (ms->mech_sess.qop == QOP_NONE) return -1; rval = saslc__buffer_fetch(ms->enc_ctx.buf_ctx, in, inlen, &buf, &buflen); if (rval == -1) return -1; if (buflen == 0) { *out = NULL; *outlen = 0; return rval; } if (encode_buffer(&ms->enc_ctx, buf, buflen, out, outlen) == -1) return -1; return rval; } /** * @brief remove integrity or confidentiality layer * @param sess session handle * @param in input buffer * @param inlen input buffer length * @param out pointer to output buffer * @param out pointer to output buffer length * @return number of bytes consumed on success, 0 if insufficient data * to process, -1 on failure * * integrity (auth-int): * len, HMAC(ki, {SeqNum, msg})[0..9], x0001, SeqNum * * confidentiality (auth-conf): * len, CIPHER(Kc, {msg, pag, HMAC(ki, {SeqNum, msg})[0..9]}), x0001, SeqNum */ static ssize_t saslc__mech_digestmd5_decode(saslc_sess_t *sess, const void *in, size_t inlen, void **out, size_t *outlen) { saslc__mech_digestmd5_sess_t *ms; uint8_t *buf; size_t buflen; ssize_t rval; ms = sess->mech_sess; assert(ms->mech_sess.qop != QOP_NONE); if (ms->mech_sess.qop == QOP_NONE) return -1; rval = saslc__buffer32_fetch(ms->dec_ctx.buf_ctx, in, inlen, &buf, &buflen); if (rval == -1) return -1; if (buflen == 0) { *out = NULL; *outlen = 0; return rval; } if (decode_buffer(&ms->dec_ctx, buf, buflen, out, outlen) == -1) return -1; return rval; } /************************************************************************ * XXX: Share with mech_gssapi.c? They are almost identical. */ /** * @brief choose the best qop based on what was provided by the * challenge and a possible user mask. * @param sess the session context * @param qop_flags the qop flags parsed from the challenge string * @return the selected saslc__mech_sess_qop_t or -1 if no match */ static int choose_qop(saslc_sess_t *sess, uint32_t qop_flags) { list_t *list; const char *user_qop; if (qop_flags == 0) /* no qop spec in challenge (it's optional) */ return QOP_NONE; qop_flags &= DEFAULT_QOP_MASK; user_qop = saslc_sess_getprop(sess, SASLC_DIGESTMD5_QOPMASK); if (user_qop != NULL) { if (saslc__list_parse(&list, user_qop) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } qop_flags &= saslc__mech_qop_list_flags(list); saslc__list_free(list); } /* * Select the most secure supported qop. */ if ((qop_flags & F_QOP_CONF) != 0) return QOP_CONF; if ((qop_flags & F_QOP_INT) != 0) return QOP_INT; if ((qop_flags & F_QOP_NONE) != 0) return QOP_NONE; saslc__error_set(ERR(sess), ERROR_MECH, "cannot choose an acceptable qop"); return -1; } /************************************************************************/ /** * @brief choose the best cipher based on what was provided by the * challenge and a possible user mask. * @param sess the session context * @param cipher_flags the cipher flags parsed from the challenge * string * @return the selected cipher_t */ static int choose_cipher(saslc_sess_t *sess, unsigned int cipher_flags) { list_t *list; unsigned int cipher_mask; const char *user_cipher; if (cipher_flags == 0) { saslc__error_set(ERR(sess), ERROR_MECH, "no cipher spec in challenge"); return -1; } cipher_mask = DEFAULT_CIPHER_MASK; user_cipher = saslc_sess_getprop(sess, SASLC_DIGESTMD5_CIPHERMASK); if (user_cipher != NULL) { if (saslc__list_parse(&list, user_cipher) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } cipher_mask = cipher_list_flags(list); saslc__list_free(list); } cipher_flags &= cipher_mask; /* * Select the most secure cipher supported. * XXX: Is the order here right? */ if ((cipher_flags & F_CIPHER_AES) != 0) return CIPHER_AES; if ((cipher_flags & F_CIPHER_3DES) != 0) return CIPHER_3DES; if ((cipher_flags & F_CIPHER_DES) != 0) return CIPHER_DES; if ((cipher_flags & F_CIPHER_RC4) != 0) return CIPHER_RC4; if ((cipher_flags & F_CIPHER_RC4_56) != 0) return CIPHER_RC4_56; if ((cipher_flags & F_CIPHER_RC4_40) != 0) return CIPHER_RC4_40; saslc__error_set(ERR(sess), ERROR_MECH, "qop \"auth-conf\" requires a cipher"); return -1; } /** * @brief get the challenge_t value corresponding to a challenge key * string. * @param key challenge key string * @return the challenge_t value including CHALLENGE_IGNORE (-1) if * the key is not recognized */ static challenge_t get_challenge_t(const char *key) { static const struct { const char *key; challenge_t value; } challenge_keys[] = { { "realm", CHALLENGE_REALM }, { "nonce", CHALLENGE_NONCE }, { "qop", CHALLENGE_QOP }, { "stale", CHALLENGE_STALE }, { "maxbuf", CHALLENGE_MAXBUF }, { "charset", CHALLENGE_CHARSET }, { "algorithm", CHALLENGE_ALGORITHM }, { "cipher", CHALLENGE_CIPHER } }; size_t i; for (i = 0; i < __arraycount(challenge_keys); i++) { if (strcasecmp(key, challenge_keys[i].key) == 0) return challenge_keys[i].value; } return CHALLENGE_IGNORE; } /** * @brief parses challenge and store result in mech_sess. * @param mech_sess session where parsed data will be stored * @param challenge challenge * @return 0 on success, -1 on failure. */ static int saslc__mech_digestmd5_parse_challenge(saslc_sess_t *sess, const char *challenge) { saslc__mech_digestmd5_sess_t *ms; list_t *list, *n; list_t *tmp_list; cdata_t *cdata; size_t maxbuf; uint32_t tmp_flags; int rv; /******************************************************************/ /* digest-challenge = */ /* 1#( realm | nonce | qop-options | stale | server_maxbuf | */ /* charset | algorithm | cipher-opts | auth-param ) */ /******************************************************************/ saslc__msg_dbg("challenge: '%s'\n", challenge); ms = sess->mech_sess; cdata = &ms->cdata; rv = -1; memset(cdata, 0, sizeof(*cdata)); if (saslc__list_parse(&list, challenge) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } saslc__list_log(list, "parse list:\n"); for (n = list; n != NULL; n = n->next) { char *key; char *val; /* Split string into key and val */ key = n->value; val = strchr(key, '='); if (val == NULL) goto no_mem; *val = '\0'; val = strip_quotes(val + 1); saslc__msg_dbg("key='%s' val='%s'\n", key, val); switch (get_challenge_t(key)) { case CHALLENGE_REALM: /**************************************************/ /* realm = "realm" "=" <"> realm-value <"> */ /* realm-value = qdstr-val */ /* */ /* This directive is optional; if not present, */ /* the client SHOULD solicit it from the user or */ /* be able to compute a default; a plausible */ /* default might be the realm supplied by the */ /* user when they logged in to the client system. */ /* Multiple realm directives are allowed, in */ /* which case the user or client must choose one */ /* as the realm for which to supply to username */ /* and password. */ /**************************************************/ if (saslc__list_append(&cdata->realm, val) == -1) goto no_mem; break; case CHALLENGE_NONCE: /**************************************************/ /* nonce = "nonce" "=" <"> nonce-value <"> */ /* nonce-value = *qdtext */ /* */ /* This directive is required and MUST appear */ /* exactly once; if not present, or if multiple */ /* instances are present, the client should abort */ /* the authentication exchange. */ /**************************************************/ if (cdata->nonce != NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "multiple nonce in challenge"); goto out; } cdata->nonce = strdup(val); if (cdata->nonce == NULL) goto no_mem; break; case CHALLENGE_QOP: /**************************************************/ /* qop-options = "qop" "=" <"> qop-list <"> */ /* qop-list = 1#qop-value */ /* qop-value = "auth" | "auth-int" | */ /* "auth-conf" | token */ /* */ /* This directive is optional; if not present it */ /* defaults to "auth". The client MUST ignore */ /* unrecognized options; if the client recognizes */ /* no option, it should abort the authentication */ /* exchange. */ /**************************************************/ if (saslc__list_parse(&tmp_list, val) == -1) goto no_mem; saslc__list_log(tmp_list, "qop list:\n"); tmp_flags = saslc__mech_qop_list_flags(tmp_list); saslc__list_free(tmp_list); if (tmp_flags == 0) { saslc__error_set(ERR(sess), ERROR_MECH, "qop required in challenge"); goto out; } cdata->qop_flags |= tmp_flags; break; case CHALLENGE_STALE: /**************************************************/ /* stale = "stale" "=" "true" */ /* */ /* This directive may appear at most once; if */ /* multiple instances are present, the client */ /* should abort the authentication exchange. */ /**************************************************/ if (cdata->stale) { saslc__error_set(ERR(sess), ERROR_MECH, "multiple stale in challenge"); goto out; } if (strcasecmp(val, "true") != 0) { saslc__error_set(ERR(sess), ERROR_MECH, "stale must be true"); goto out; } cdata->stale = true; break; case CHALLENGE_MAXBUF: /**************************************************/ /* maxbuf-value = 1*DIGIT */ /* */ /* The value MUST be bigger than 16 and smaller */ /* or equal to 16777215 (i.e. 2**24-1). If this */ /* directive is missing, the default value is */ /* 65536. This directive may appear at most once; */ /* if multiple instances are present, the client */ /* MUST abort the authentication exchange. */ /**************************************************/ if (cdata->maxbuf != 0) { saslc__error_set(ERR(sess), ERROR_MECH, "multiple maxbuf in challenge"); goto out; } maxbuf = (size_t)strtoul(val, NULL, 10); if (INVALID_MAXBUF(maxbuf)) { saslc__error_set(ERR(sess), ERROR_MECH, "invalid maxbuf in challenge"); goto out; } cdata->maxbuf = maxbuf; break; case CHALLENGE_CHARSET: /**************************************************/ /* charset = "charset" "=" "utf-8" */ /* */ /* This directive may appear at most once; if */ /* multiple instances are present, the client */ /* should abort the authentication exchange. */ /**************************************************/ if (cdata->utf8) { saslc__error_set(ERR(sess), ERROR_MECH, "multiple charset in challenge"); goto out; } if (strcasecmp(val, "utf-8") != 0) { saslc__error_set(ERR(sess), ERROR_MECH, "charset != \"utf-8\" in challenge"); goto out; } cdata->utf8 = true; break; case CHALLENGE_ALGORITHM: /**************************************************/ /* algorithm = "algorithm" "=" "md5-sess" */ /* */ /* This directive is required and MUST appear */ /* exactly once; if not present, or if multiple */ /* instances are present, the client should abort */ /* the authentication exchange. */ /**************************************************/ if (cdata->algorithm) { saslc__error_set(ERR(sess), ERROR_MECH, "multiple algorithm in challenge"); goto out; } if (strcasecmp(val, "md5-sess") != 0) { saslc__error_set(ERR(sess), ERROR_MECH, "algorithm != \"md5-sess\" in challenge"); goto out; } cdata->algorithm = true; break; case CHALLENGE_CIPHER: /**************************************************/ /* cipher-opts = "cipher" "=" <"> 1#cipher-val <">*/ /* cipher-val = "3des" | "des" | "rc4-40" | */ /* "rc4" |"rc4-56" | "aes" | */ /* token */ /* */ /* This directive must be present exactly once if */ /* "auth-conf" is offered in the "qop-options" */ /* directive, in which case the "3des" cipher is */ /* mandatory-to-implement. The client MUST ignore */ /* unrecognized options; if the client recognizes */ /* no option, it should abort the authentication */ /* exchange. */ /**************************************************/ if (saslc__list_parse(&tmp_list, val) == -1) goto no_mem; saslc__list_log(tmp_list, "cipher list:\n"); tmp_flags = cipher_list_flags(tmp_list); saslc__list_free(tmp_list); if (tmp_flags == 0) { saslc__error_set(ERR(sess), ERROR_MECH, "unknown cipher"); goto out; } cdata->cipher_flags |= tmp_flags; break; case CHALLENGE_IGNORE: /**************************************************/ /* auth-param = token "=" ( token | */ /* quoted-string ) */ /* */ /* The client MUST ignore any unrecognized */ /* directives. */ /**************************************************/ break; } } /* * make sure realms are in iso8859-1 */ if (stringprep_realms(cdata->utf8, cdata->realm) == -1) { saslc__error_set(ERR(sess), ERROR_MECH, "unable to convert realms in challenge from " "\"utf-8\" to iso8859-1"); goto out; } /* * test for required options */ if (cdata->nonce == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "nonce required in challenge"); goto out; } if (!cdata->algorithm) { saslc__error_set(ERR(sess), ERROR_MECH, "algorithm required in challenge"); goto out; } /* * set the default maxbuf value if it was missing from the * challenge. */ if (cdata->maxbuf == 0) cdata->maxbuf = DEFAULT_MAXBUF; saslc__msg_dbg("qop_flags=0x%04x\n", cdata->qop_flags); saslc__msg_dbg("cipher_flags=0x%04x\n", cdata->cipher_flags); rv = 0; out: saslc__list_free(list); return rv; no_mem: saslc__error_set_errno(ERR(sess), ERROR_NOMEM); goto out; } /** * @brief creates digestmd5 mechanism session. * Function initializes also default options for the session. * @param sess sasl session * @return 0 on success, -1 on failure. */ static int saslc__mech_digestmd5_create(saslc_sess_t *sess) { saslc__mech_digestmd5_sess_t *c; if ((c = calloc(1, sizeof(*c))) == NULL) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } c->rdata.nonce_cnt = 1; sess->mech_sess = c; return 0; } static void free_cdata(cdata_t *cdata) { free(cdata->nonce); saslc__list_free(cdata->realm); } static void free_rdata(rdata_t *rdata) { free(rdata->authcid); free(rdata->authzid); free(rdata->cnonce); free(rdata->digesturi); if (rdata->passwd != NULL) { memset(rdata->passwd, 0, strlen(rdata->passwd)); free(rdata->passwd); } free(rdata->realm); } /** * @brief destroys digestmd5 mechanism session. * Function also is freeing assigned resources to the session. * @param sess sasl session * @return Functions always returns 0. */ static int saslc__mech_digestmd5_destroy(saslc_sess_t *sess) { saslc__mech_digestmd5_sess_t *ms; ms = sess->mech_sess; free_cdata(&ms->cdata); free_rdata(&ms->rdata); saslc__buffer32_destroy(ms->dec_ctx.buf_ctx); saslc__buffer_destroy(ms->enc_ctx.buf_ctx); cipher_context_destroy(ms->dec_ctx.cph_ctx); cipher_context_destroy(ms->enc_ctx.cph_ctx); free(sess->mech_sess); sess->mech_sess = NULL; return 0; } /** * @brief collect the response data necessary to build the reply. * @param sess the session context * @return 0 on success, -1 on failure * * NOTE: * The input info is from the challenge (previously saved in cdata of * saslc__mech_digestmd5_sess_t) or from the property dictionaries. * * The output info is saved in (mostly) in rdata of the * saslc__mech_digestmd5_sess_t structure. The qop is special in that * it is exposed to the saslc__mech_sess_t layer. */ static int saslc__mech_digestmd5_response_data(saslc_sess_t *sess) { saslc__mech_digestmd5_sess_t *ms; cdata_t *cdata; rdata_t *rdata; const char *authcid; const char *authzid; const char *hostname; const char *maxbuf; const char *passwd; int rv; ms = sess->mech_sess; cdata = &ms->cdata; rdata = &ms->rdata; if ((rv = choose_qop(sess, cdata->qop_flags)) == -1) return -1; /* error message already set */ ms->mech_sess.qop = rv; if (ms->mech_sess.qop == QOP_CONF) { if ((rv = choose_cipher(sess, cdata->cipher_flags)) == -1) return -1; /* error message already set */ rdata->cipher = rv; } hostname = saslc_sess_getprop(sess, SASLC_DIGESTMD5_HOSTNAME); if (hostname == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "hostname is required for authentication"); return -1; } rdata->realm = choose_realm(sess, hostname, cdata->realm); if (rdata->realm == NULL) return -1; /* error message already set */ rdata->digesturi = saslc__mech_digestmd5_digesturi(sess, hostname); if (rdata->digesturi == NULL) return -1; /* error message already set */ authcid = saslc_sess_getprop(sess, SASLC_DIGESTMD5_AUTHCID); if (authcid == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "authcid is required for an authentication"); return -1; } rdata->authcid = strdup(authcid); if (rdata->authcid == NULL) goto no_mem; authzid = saslc_sess_getprop(sess, SASLC_DIGESTMD5_AUTHZID); if (authzid != NULL) { rdata->authzid = strdup(authzid); if (rdata->authzid == NULL) goto no_mem; } passwd = saslc_sess_getprop(sess, SASLC_DIGESTMD5_PASSWD); if (passwd == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "password is required for an authentication"); return -1; } rdata->passwd = strdup(passwd); if (rdata->passwd == NULL) goto no_mem; rdata->cnonce = saslc__mech_digestmd5_nonce(NONCE_LEN); if (rdata->cnonce == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "failed to create cnonce"); return -1; } #ifdef SASLC_DIGESTMD5_CNONCE /* XXX: for debugging! */ { const char *cnonce; cnonce = saslc_sess_getprop(sess, SASLC_DIGESTMD5_CNONCE); if (cnonce != NULL) { rdata->cnonce = strdup(cnonce); if (rdata->cnonce == NULL) goto no_mem; } } #endif if (ms->mech_sess.qop != QOP_NONE) { maxbuf = saslc_sess_getprop(sess, SASLC_DIGESTMD5_MAXBUF); if (maxbuf != NULL) rdata->maxbuf = (size_t)strtoul(maxbuf, NULL, 10); if (rdata->maxbuf == 0) rdata->maxbuf = cdata->maxbuf; if (INVALID_MAXBUF(rdata->maxbuf)) { saslc__error_set(ERR(sess), ERROR_MECH, "maxbuf out of range"); return -1; } } return 0; no_mem: saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } /** * @brief compute the maximum payload that can go into an integrity or * confidentiality packet. * @param maxbuf the server's maxbuf size. * @param blksize the ciphers block size. 0 or 1 if there is no blocking. * @return the payload size * * The packet (not including the leading uint32_t packet length field) * has this structure: * * struct { * uint8_t payload[]; // packet payload * uint8_t padding[]; // padding to block size * uint8_t hmac_0_9[10]; // the first 10 bytes of the hmac * uint8_t version[2]; // version number (1) in BE format * uint8_t seqnum[4]; // sequence number in BE format * } __packed * * NOTE: if the block size is > 1, then padding is required to make * the {payload[], padding[], and hmac_0_9[]} a multiple of the block * size. Furthermore there must be at least one byte of padding! The * padding bytes are all set to the padding length and one byte of * padding is necessary to recover the padding length. */ static size_t maxpayload(size_t maxbuf, size_t blksize) { size_t l; if (blksize <= 1) { /* no padding used */ if (maxbuf <= sizeof(md5hash_t)) return 0; return maxbuf - sizeof(md5hash_t); } if (maxbuf < 2 * blksize + 6) return 0; l = rounddown(maxbuf - 6, blksize); if (l <= 10 + 1) /* we need at least one byte of padding */ return 0; return l - 10 - 1; } /** * @brief initialize the encode and decode coder contexts for the session * @param sess the current session * @return 0 on success, -1 on failure. */ static int init_coder_context(saslc_sess_t *sess) { saslc__mech_digestmd5_sess_t *ms; size_t blksize; #ifdef SASLC_DIGESTMD5_SELFTEST int selftest; /* XXX: allow for testing against ourselves */ #endif ms = sess->mech_sess; #ifdef SASLC_DIGESTMD5_SELFTEST selftest = saslc_sess_getprop(sess, SASLC_DIGESTMD5_SELFTEST) != NULL; #endif blksize = 0; switch (ms->mech_sess.qop) { case QOP_NONE: return 0; case QOP_INT: #ifdef SASLC_DIGESTMD5_SELFTEST ms->dec_ctx.key = selftest ? ms->keys.kic : ms->keys.kis; #else ms->dec_ctx.key = ms->keys.kis; #endif ms->enc_ctx.key = ms->keys.kic; ms->dec_ctx.cph_ctx = NULL; ms->enc_ctx.cph_ctx = NULL; break; case QOP_CONF: #ifdef SASLC_DIGESTMD5_SELFTEST ms->dec_ctx.key = selftest ? ms->keys.kcc : ms->keys.kcs; #else ms->dec_ctx.key = ms->keys.kcs; #endif ms->enc_ctx.key = ms->keys.kcc; ms->dec_ctx.cph_ctx = cipher_context_create(sess, ms->rdata.cipher, 0, ms->dec_ctx.key); if (ms->dec_ctx.cph_ctx == NULL) return -1; ms->enc_ctx.cph_ctx = cipher_context_create(sess, ms->rdata.cipher, 1, ms->enc_ctx.key); if (ms->enc_ctx.cph_ctx == NULL) return -1; blksize = ms->enc_ctx.cph_ctx->blksize; break; } ms->dec_ctx.sess = sess; ms->enc_ctx.sess = sess; ms->dec_ctx.buf_ctx = saslc__buffer32_create(sess, ms->rdata.maxbuf); if (ms->cdata.maxbuf < 2 * blksize + 6) { saslc__error_set(ERR(sess), ERROR_MECH, "server buffer too small for packet"); return -1; } ms->enc_ctx.buf_ctx = saslc__buffer_create(sess, maxpayload(ms->cdata.maxbuf, blksize)); if (ms->dec_ctx.buf_ctx == NULL || ms->enc_ctx.buf_ctx == NULL) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); return -1; } return 0; } /** * @brief construct the reply string. * @param sess session context * @param response string * @return reply string or NULL on failure. */ static char * saslc__mech_digestmd5_reply(saslc_sess_t *sess, char *response) { saslc__mech_digestmd5_sess_t *ms; char *out; char *cipher, *maxbuf, *realm; ms = sess->mech_sess; out = NULL; cipher = __UNCONST(""); maxbuf = __UNCONST(""); realm = __UNCONST(""); switch (ms->mech_sess.qop) { case QOP_CONF: if (asprintf(&cipher, "cipher=\"%s\",", cipher_name(ms->rdata.cipher)) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); goto done; } /*FALLTHROUGH*/ case QOP_INT: if (asprintf(&maxbuf, "maxbuf=%zu,", ms->rdata.maxbuf) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); goto done; } break; case QOP_NONE: break; default: assert(/*CONSTCOND*/0); return NULL; } if (ms->rdata.realm != NULL && asprintf(&realm, "realm=\"%s\",", ms->rdata.realm) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); goto done; } if (asprintf(&out, "username=\"%s\"," "%s" /* realm= */ "nonce=\"%s\"," "cnonce=\"%s\"," "nc=%08d," "qop=%s," "%s" /* cipher= */ "%s" /* maxbuf= */ "digest-uri=\"%s\"," "response=%s", ms->rdata.authcid, realm, ms->cdata.nonce, ms->rdata.cnonce, ms->rdata.nonce_cnt, saslc__mech_qop_name(ms->mech_sess.qop), cipher, maxbuf, ms->rdata.digesturi, response ) == -1) { saslc__error_set_errno(ERR(sess), ERROR_NOMEM); out = NULL; } done: if (realm[0] != '\0') free(realm); if (maxbuf[0] != '\0') free(maxbuf); if (cipher[0] != '\0') free(cipher); return out; } /** * @brief do one step of the sasl authentication * @param sess sasl session * @param in input data * @param inlen input data length * @param out place to store output data * @param outlen output data length * @return MECH_OK - authentication successful, * MECH_STEP - more steps are needed, * MECH_ERROR - error */ static int saslc__mech_digestmd5_cont(saslc_sess_t *sess, const void *in, size_t inlen, void **out, size_t *outlen) { saslc__mech_digestmd5_sess_t *ms; char *response; const char *p; ms = sess->mech_sess; switch(ms->mech_sess.step) { case 0: /* in case we are called before getting data from server */ if (inlen == 0) { *out = NULL; *outlen = 0; return MECH_STEP; } /* if input data was provided, then doing the first step */ ms->mech_sess.step++; /*FALLTHROUGH*/ case 1: if (saslc__mech_digestmd5_parse_challenge(sess, in) == -1) return MECH_ERROR; if (saslc__mech_digestmd5_response_data(sess) == -1) return MECH_ERROR; if ((response = saslc__mech_digestmd5_response(ms, "AUTHENTICATE")) == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "unable to construct response"); return MECH_ERROR; } *out = saslc__mech_digestmd5_reply(sess, response); free(response); if (*out == NULL) return MECH_ERROR; *outlen = strlen(*out); return MECH_STEP; case 2: if ((response = saslc__mech_digestmd5_response(ms, "")) == NULL) { saslc__error_set(ERR(sess), ERROR_MECH, "unable to construct rspauth"); return MECH_ERROR; } p = in; if (strncmp(p, "rspauth=", 8) != 0 || strcmp(response, p + 8) != 0) { saslc__msg_dbg("rspauth='%s'\n", response); saslc__error_set(ERR(sess), ERROR_MECH, "failed to validate rspauth response"); free(response); return MECH_ERROR; } free(response); if (init_coder_context(sess) == -1) return MECH_ERROR; *out = NULL; *outlen = 0; return MECH_OK; default: assert(/*CONSTCOND*/0); /* impossible */ return MECH_ERROR; } } /* mechanism definition */ const saslc__mech_t saslc__mech_digestmd5 = { .name = "DIGEST-MD5", .flags = FLAG_MUTUAL | FLAG_DICTIONARY, .create = saslc__mech_digestmd5_create, .cont = saslc__mech_digestmd5_cont, .encode = saslc__mech_digestmd5_encode, .decode = saslc__mech_digestmd5_decode, .destroy = saslc__mech_digestmd5_destroy };