/*
 *   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: 572e04d72b0876a462d395080ace3137829bddb9 $
 * @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: 572e04d72b0876a462d395080ace3137829bddb9 $")

#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
	},
};
