diff options
Diffstat (limited to 'src/modules/rlm_digest/rlm_digest.c')
-rw-r--r-- | src/modules/rlm_digest/rlm_digest.c | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/src/modules/rlm_digest/rlm_digest.c b/src/modules/rlm_digest/rlm_digest.c new file mode 100644 index 0000000..572e04d --- /dev/null +++ b/src/modules/rlm_digest/rlm_digest.c @@ -0,0 +1,601 @@ +/* + * This program is is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * @file rlm_digest.c + * @brief Handles SIP digest authentication requests from Cisco SIP servers. + * + * @copyright 2002,2006 The FreeRADIUS server project + * @copyright 2002 Alan DeKok <aland@ox.org> + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/md5.h> + +static int digest_fix(REQUEST *request) +{ + VALUE_PAIR *first, *i; + vp_cursor_t cursor; + + /* + * We need both of these attributes to do the authentication. + */ + first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY); + if (!first) { + return RLM_MODULE_NOOP; + } + + /* + * Check the sanity of the attribute. + */ + if (first->vp_length != 32) { + return RLM_MODULE_NOOP; + } + + /* + * Check for proper format of the Digest-Attributes + */ + RDEBUG("Checking for correctly formatted Digest-Attributes"); + + first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY); + if (!first) { + return RLM_MODULE_NOOP; + } + + fr_cursor_init(&cursor, &first); + while ((i = fr_cursor_next_by_num(&cursor, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY))) { + int length = i->vp_length; + int attrlen; + uint8_t const *p = i->vp_octets; + + /* + * Until this stupidly encoded attribute is exhausted. + */ + while (length > 0) { + /* + * The attribute type must be valid + */ + if ((p[0] == 0) || (p[0] > 10)) { + RDEBUG("Not formatted as Digest-Attributes: TLV type (%u) invalid", (unsigned int) p[0]); + return RLM_MODULE_NOOP; + } + + attrlen = p[1]; /* stupid VSA format */ + + /* + * Too short. + */ + if (attrlen < 3) { + RDEBUG("Not formatted as Digest-Attributes: TLV too short"); + return RLM_MODULE_NOOP; + } + + /* + * Too long. + */ + if (attrlen > length) { + RDEBUG("Not formatted as Digest-Attributes: TLV too long)"); + return RLM_MODULE_NOOP; + } + + length -= attrlen; + p += attrlen; + } /* loop over this one attribute */ + } + + /* + * Convert them to something sane. + */ + RDEBUG("Digest-Attributes look OK. Converting them to something more useful"); + fr_cursor_first(&cursor); + while ((i = fr_cursor_next_by_num(&cursor, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY))) { + int length = i->vp_length; + int attrlen; + uint8_t const *p = &i->vp_octets[0]; + VALUE_PAIR *sub; + + /* + * Until this stupidly encoded attribute is exhausted. + */ + while (length > 0) { + /* + * The attribute type must be valid + */ + if ((p[0] == 0) || (p[0] > 10)) { + REDEBUG("Received Digest-Attributes with invalid sub-attribute %d", p[0]); + return RLM_MODULE_INVALID; + } + + attrlen = p[1]; /* stupid VSA format */ + + /* + * Too short. + */ + if (attrlen < 3) { + REDEBUG("Received Digest-Attributes with short sub-attribute %d, of length %d", p[0], attrlen); + return RLM_MODULE_INVALID; + } + + /* + * Too long. + */ + if (attrlen > length) { + REDEBUG("Received Digest-Attributes with long sub-attribute %d, of length %d", p[0], attrlen); + return RLM_MODULE_INVALID; + } + + /* + * Create a new attribute, broken out of + * the stupid sub-attribute crap. + * + * Didn't they know that VSA's exist? + */ + sub = radius_pair_create(request->packet, &request->packet->vps, + PW_DIGEST_REALM - 1 + p[0], 0); + fr_pair_value_bstrncpy(sub, p + 2, attrlen - 2); + + if ((rad_debug_lvl > 1) && fr_log_fp) { + vp_print(fr_log_fp, sub); + } + + /* + * FIXME: Check for the existence + * of the necessary attributes! + */ + + length -= attrlen; + p += attrlen; + } /* loop over this one attribute */ + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) mod_authorize(UNUSED void *instance, REQUEST *request) +{ + rlm_rcode_t rcode; + + /* + * Double-check and fix the attributes. + */ + rcode = digest_fix(request); + if (rcode != RLM_MODULE_OK) return rcode; + + + if (fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) { + RWDEBUG2("Auth-Type already set. Not setting to DIGEST"); + return RLM_MODULE_NOOP; + } + + /* + * Everything's OK, add a digest authentication type. + */ + RDEBUG("Adding Auth-Type = DIGEST"); + pair_make_config("Auth-Type", "DIGEST", T_OP_EQ); + + return RLM_MODULE_OK; +} + +/* + * Perform all of the wondrous variants of digest authentication. + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request) +{ + int i; + size_t a1_len, a2_len, kd_len; + uint8_t a1[(MAX_STRING_LEN + 1) * 5]; /* can be 5 attributes */ + uint8_t a2[(MAX_STRING_LEN + 1) * 3]; /* can be 3 attributes */ + uint8_t kd[(MAX_STRING_LEN + 1) * 5]; + uint8_t hash[16]; /* MD5 output */ + VALUE_PAIR *vp, *passwd, *algo; + VALUE_PAIR *qop, *nonce; + + /* + * We require access to the plain-text password, or to the + * Digest-HA1 parameter. + */ + passwd = fr_pair_find_by_num(request->config, PW_DIGEST_HA1, 0, TAG_ANY); + if (passwd) { + if (passwd->vp_length != 32) { + RAUTH("Digest-HA1 has invalid length, authentication failed"); + return RLM_MODULE_INVALID; + } + } else { + passwd = fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY); + } + if (!passwd) { + RAUTH("Cleartext-Password or Digest-HA1 is required for authentication"); + return RLM_MODULE_INVALID; + } + + /* + * We need these, too. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY); + if (!vp) { + error: + REDEBUG("You set 'Auth-Type = Digest' for a request that does not contain any digest attributes!"); + return RLM_MODULE_INVALID; + } + + /* + * Look for the "internal" FreeRADIUS Digest attributes. + * If they don't exist, it means that someone forced + * Auth-Type = digest, without putting "digest" into the + * "authorize" section. In that case, try to decode the + * attributes here. + */ + if (!fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY)) { + int rcode; + + rcode = digest_fix(request); + + /* + * NOOP means "couldn't find the attributes". + * That's bad. + */ + if (rcode == RLM_MODULE_NOOP) goto error; + + if (rcode != RLM_MODULE_OK) return rcode; + } + + /* + * We require access to the Digest-Nonce-Value + */ + nonce = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY); + if (!nonce) { + REDEBUG("No Digest-Nonce: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + + /* + * A1 = Digest-User-Name ":" Realm ":" Password + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_USER_NAME, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-User-Name: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a1[0], vp->vp_octets, vp->vp_length); + a1_len = vp->vp_length; + + a1[a1_len] = ':'; + a1_len++; + + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_REALM, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-Realm: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length); + a1_len += vp->vp_length; + + a1[a1_len] = ':'; + a1_len++; + + if (passwd->da->attr == PW_CLEARTEXT_PASSWORD) { + memcpy(&a1[a1_len], passwd->vp_octets, passwd->vp_length); + a1_len += passwd->vp_length; + a1[a1_len] = '\0'; + RDEBUG2("A1 = %s", a1); + } else { + a1[a1_len] = '\0'; + RDEBUG2("A1 = %s (using Digest-HA1)", a1); + a1_len = 16; + } + + /* + * See which variant we calculate. + * Assume MD5 if no Digest-Algorithm attribute received + */ + algo = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ALGORITHM, 0, TAG_ANY); + if ((!algo) || + (strcasecmp(algo->vp_strvalue, "MD5") == 0)) { + /* + * Set A1 to Digest-HA1 if no User-Password found + */ + if (passwd->da->attr == PW_DIGEST_HA1) { + if (fr_hex2bin(&a1[0], sizeof(a1), passwd->vp_strvalue, passwd->vp_length) != 16) { + RDEBUG2("Invalid text in Digest-HA1"); + return RLM_MODULE_INVALID; + } + } + + } else if (strcasecmp(algo->vp_strvalue, "MD5-sess") == 0) { + /* + * K1 = H(A1) : Digest-Nonce ... : H(A2) + * + * If we find Digest-HA1, we assume it contains + * H(A1). + */ + if (passwd->da->attr == PW_CLEARTEXT_PASSWORD) { + fr_md5_calc(hash, &a1[0], a1_len); + fr_bin2hex((char *) &a1[0], hash, 16); + } else { /* MUST be Digest-HA1 */ + memcpy(&a1[0], passwd->vp_strvalue, 32); + } + a1_len = 32; + + a1[a1_len] = ':'; + a1_len++; + + /* + * Tack on the Digest-Nonce. Length must be even + */ + if ((nonce->vp_length & 1) != 0) { + REDEBUG("Received Digest-Nonce hex string with invalid length: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a1[a1_len], nonce->vp_octets, nonce->vp_length); + a1_len += nonce->vp_length; + + a1[a1_len] = ':'; + a1_len++; + + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-CNonce: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + + /* + * Digest-CNonce length must be even + */ + if ((vp->vp_length & 1) != 0) { + REDEBUG("Received Digest-CNonce hex string with invalid length: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length); + a1_len += vp->vp_length; + + } else if (strcasecmp(algo->vp_strvalue, "MD5") != 0) { + /* + * We check for "MD5-sess" and "MD5". + * Anything else is an error. + */ + REDEBUG("Unknown Digest-Algorithm \"%s\": Cannot perform Digest authentication", vp->vp_strvalue); + return RLM_MODULE_INVALID; + } + + /* + * A2 = Digest-Method ":" Digest-URI + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_METHOD, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-Method: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a2[0], vp->vp_octets, vp->vp_length); + a2_len = vp->vp_length; + + a2[a2_len] = ':'; + a2_len++; + + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_URI, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-URI: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&a2[a2_len], vp->vp_octets, vp->vp_length); + a2_len += vp->vp_length; + + /* + * QOP is "auth-int", tack on ": Digest-Body-Digest" + */ + qop = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_QOP, 0, TAG_ANY); + if (qop) { + if (strcasecmp(qop->vp_strvalue, "auth-int") == 0) { + VALUE_PAIR *body; + + /* + * Add in Digest-Body-Digest + */ + a2[a2_len] = ':'; + a2_len++; + + /* + * Must be a hex representation of an MD5 digest. + */ + body = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_BODY_DIGEST, 0, TAG_ANY); + if (!body) { + REDEBUG("No Digest-Body-Digest: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + + if ((a2_len + body->vp_length) > sizeof(a2)) { + REDEBUG("Digest-Body-Digest is too long"); + return RLM_MODULE_INVALID; + } + + memcpy(a2 + a2_len, body->vp_octets, body->vp_length); + a2_len += body->vp_length; + + } else if (strcasecmp(qop->vp_strvalue, "auth") != 0) { + REDEBUG("Unknown Digest-QOP \"%s\": Cannot perform Digest authentication", qop->vp_strvalue); + return RLM_MODULE_INVALID; + } + } + + a2[a2_len] = '\0'; + RDEBUG2("A2 = %s", a2); + + /* + * KD = H(A1) : Digest-Nonce ... : H(A2). + * Compute MD5 if Digest-Algorithm == "MD5-Sess", + * or if we found a User-Password. + */ + if (((algo != NULL) && + (strcasecmp(algo->vp_strvalue, "MD5-Sess") == 0)) || + (passwd->da->attr == PW_CLEARTEXT_PASSWORD)) { + a1[a1_len] = '\0'; + fr_md5_calc(&hash[0], &a1[0], a1_len); + } else { + memcpy(&hash[0], &a1[0], a1_len); + } + fr_bin2hex((char *) kd, hash, sizeof(hash)); + +#ifndef NRDEBUG + if (rad_debug_lvl > 1) { + fr_printf_log("H(A1) = "); + for (i = 0; i < 16; i++) { + fr_printf_log("%02x", hash[i]); + } + fr_printf_log("\n"); + } +#endif + kd_len = 32; + + kd[kd_len] = ':'; + kd_len++; + + memcpy(&kd[kd_len], nonce->vp_octets, nonce->vp_length); + kd_len += nonce->vp_length; + + /* + * No QOP defined. Do RFC 2069 compatibility. + */ + if (!qop) { + /* + * Do nothing here. + */ + + } else { /* Digest-QOP MUST be "auth" or "auth-int" */ + /* + * Tack on ":" Digest-Nonce-Count ":" Digest-CNonce + * ":" Digest-QOP + */ + kd[kd_len] = ':'; + kd_len++; + + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE_COUNT, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-Nonce-Count: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length); + kd_len += vp->vp_length; + + kd[kd_len] = ':'; + kd_len++; + + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-CNonce: Cannot perform Digest authentication"); + return RLM_MODULE_INVALID; + } + memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length); + kd_len += vp->vp_length; + + kd[kd_len] = ':'; + kd_len++; + + memcpy(&kd[kd_len], qop->vp_octets, qop->vp_length); + kd_len += qop->vp_length; + } + + /* + * Tack on ":" H(A2) + */ + kd[kd_len] = ':'; + kd_len++; + + fr_md5_calc(&hash[0], &a2[0], a2_len); + + fr_bin2hex((char *) kd + kd_len, hash, sizeof(hash)); + +#ifndef NRDEBUG + if (rad_debug_lvl > 1) { + fr_printf_log("H(A2) = "); + for (i = 0; i < 16; i++) { + fr_printf_log("%02x", hash[i]); + } + fr_printf_log("\n"); + } +#endif + kd_len += 32; + + kd[kd_len] = 0; + + RDEBUG2("KD = %s\n", &kd[0]); + + /* + * Take the hash of KD. + */ + fr_md5_calc(&hash[0], &kd[0], kd_len); + memcpy(&kd[0], &hash[0], 16); + + /* + * Get the binary value of Digest-Response + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY); + if (!vp) { + REDEBUG("No Digest-Response attribute in the request. Cannot perform digest authentication"); + return RLM_MODULE_INVALID; + } + + if (fr_hex2bin(&hash[0], sizeof(hash), vp->vp_strvalue, vp->vp_length) != (vp->vp_length >> 1)) { + RDEBUG2("Invalid text in Digest-Response"); + return RLM_MODULE_INVALID; + } + +#ifndef NRDEBUG + if (rad_debug_lvl > 1) { + fr_printf_log("EXPECTED "); + for (i = 0; i < 16; i++) { + fr_printf_log("%02x", kd[i]); + } + fr_printf_log("\n"); + + fr_printf_log("RECEIVED "); + for (i = 0; i < 16; i++) { + fr_printf_log("%02x", hash[i]); + } + fr_printf_log("\n"); + } +#endif + + /* + * And finally, compare the digest in the packet with KD. + */ + if (memcmp(&kd[0], &hash[0], 16) == 0) { + return RLM_MODULE_OK; + } + + RDEBUG("FAILED authentication"); + return RLM_MODULE_REJECT; +} + +/* + * The module name should be the only globally exported symbol. + * That is, everything else should be 'static'. + * + * If the module needs to temporarily modify it's instantiation + * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. + * The server will then take care of ensuring that the module + * is single-threaded. + */ +extern module_t rlm_digest; +module_t rlm_digest = { + .magic = RLM_MODULE_INIT, + .name = "digest", + .methods = { + [MOD_AUTHENTICATE] = mod_authenticate, + [MOD_AUTHORIZE] = mod_authorize + }, +}; |