2 * This program is is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License as published by
4 * the Free Software Foundation; either version 2 of the License, or (at
5 * your option) any later version.
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20 * @brief Handles SIP digest authentication requests from Cisco SIP servers.
22 * @copyright 2002,2006 The FreeRADIUS server project
23 * @copyright 2002 Alan DeKok <aland@ox.org>
27 #include <freeradius-devel/radiusd.h>
28 #include <freeradius-devel/modules.h>
29 #include <freeradius-devel/md5.h>
31 static int digest_fix(REQUEST *request)
33 VALUE_PAIR *first, *i;
37 * We need both of these attributes to do the authentication.
39 first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY);
41 return RLM_MODULE_NOOP;
45 * Check the sanity of the attribute.
47 if (first->vp_length != 32) {
48 return RLM_MODULE_NOOP;
52 * Check for proper format of the Digest-Attributes
54 RDEBUG("Checking for correctly formatted Digest-Attributes");
56 first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY);
58 return RLM_MODULE_NOOP;
61 fr_cursor_init(&cursor, &first);
62 while ((i = fr_cursor_next_by_num(&cursor, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY))) {
63 int length = i->vp_length;
65 uint8_t const *p = i->vp_octets;
68 * Until this stupidly encoded attribute is exhausted.
72 * The attribute type must be valid
74 if ((p[0] == 0) || (p[0] > 10)) {
75 RDEBUG("Not formatted as Digest-Attributes: TLV type (%u) invalid", (unsigned int) p[0]);
76 return RLM_MODULE_NOOP;
79 attrlen = p[1]; /* stupid VSA format */
85 RDEBUG("Not formatted as Digest-Attributes: TLV too short");
86 return RLM_MODULE_NOOP;
92 if (attrlen > length) {
93 RDEBUG("Not formatted as Digest-Attributes: TLV too long)");
94 return RLM_MODULE_NOOP;
99 } /* loop over this one attribute */
103 * Convert them to something sane.
105 RDEBUG("Digest-Attributes look OK. Converting them to something more useful");
106 fr_cursor_first(&cursor);
107 while ((i = fr_cursor_next_by_num(&cursor, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY))) {
108 int length = i->vp_length;
110 uint8_t const *p = &i->vp_octets[0];
115 * Until this stupidly encoded attribute is exhausted.
119 * The attribute type must be valid
121 if ((p[0] == 0) || (p[0] > 10)) {
122 REDEBUG("Received Digest-Attributes with invalid sub-attribute %d", p[0]);
123 return RLM_MODULE_INVALID;
126 attrlen = p[1]; /* stupid VSA format */
132 REDEBUG("Received Digest-Attributes with short sub-attribute %d, of length %d", p[0], attrlen);
133 return RLM_MODULE_INVALID;
139 if (attrlen > length) {
140 REDEBUG("Received Digest-Attributes with long sub-attribute %d, of length %d", p[0], attrlen);
141 return RLM_MODULE_INVALID;
145 * Create a new attribute, broken out of
146 * the stupid sub-attribute crap.
148 * Didn't they know that VSA's exist?
150 sub = radius_pair_create(request->packet, &request->packet->vps,
151 PW_DIGEST_REALM - 1 + p[0], 0);
152 sub->vp_length = attrlen - 2;
153 sub->vp_strvalue = q = talloc_array(sub, char, sub->vp_length + 1);
154 memcpy(q, p + 2, attrlen - 2);
155 q[attrlen - 2] = '\0';
157 if ((rad_debug_lvl > 1) && fr_log_fp) {
158 vp_print(fr_log_fp, sub);
162 * FIXME: Check for the existence
163 * of the necessary attributes!
168 } /* loop over this one attribute */
171 return RLM_MODULE_OK;
174 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(UNUSED void *instance, REQUEST *request)
179 * Double-check and fix the attributes.
181 rcode = digest_fix(request);
182 if (rcode != RLM_MODULE_OK) return rcode;
185 if (fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) {
186 RWDEBUG2("Auth-Type already set. Not setting to DIGEST");
187 return RLM_MODULE_NOOP;
191 * Everything's OK, add a digest authentication type.
193 RDEBUG("Adding Auth-Type = DIGEST");
194 pair_make_config("Auth-Type", "DIGEST", T_OP_EQ);
196 return RLM_MODULE_OK;
200 * Perform all of the wondrous variants of digest authentication.
202 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request)
205 size_t a1_len, a2_len, kd_len;
206 uint8_t a1[(MAX_STRING_LEN + 1) * 5]; /* can be 5 attributes */
207 uint8_t a2[(MAX_STRING_LEN + 1) * 3]; /* can be 3 attributes */
208 uint8_t kd[(MAX_STRING_LEN + 1) * 5];
209 uint8_t hash[16]; /* MD5 output */
210 VALUE_PAIR *vp, *passwd, *algo;
211 VALUE_PAIR *qop, *nonce;
214 * We require access to the plain-text password, or to the
215 * Digest-HA1 parameter.
217 passwd = fr_pair_find_by_num(request->config, PW_DIGEST_HA1, 0, TAG_ANY);
219 if (passwd->vp_length != 32) {
220 RAUTH("Digest-HA1 has invalid length, authentication failed");
221 return RLM_MODULE_INVALID;
224 passwd = fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY);
227 RAUTH("Cleartext-Password or Digest-HA1 is required for authentication");
228 return RLM_MODULE_INVALID;
232 * We need these, too.
234 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY);
237 REDEBUG("You set 'Auth-Type = Digest' for a request that does not contain any digest attributes!");
238 return RLM_MODULE_INVALID;
242 * Look for the "internal" FreeRADIUS Digest attributes.
243 * If they don't exist, it means that someone forced
244 * Auth-Type = digest, without putting "digest" into the
245 * "authorize" section. In that case, try to decode the
248 if (!fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY)) {
251 rcode = digest_fix(request);
254 * NOOP means "couldn't find the attributes".
257 if (rcode == RLM_MODULE_NOOP) goto error;
259 if (rcode != RLM_MODULE_OK) return rcode;
263 * We require access to the Digest-Nonce-Value
265 nonce = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY);
267 REDEBUG("No Digest-Nonce: Cannot perform Digest authentication");
268 return RLM_MODULE_INVALID;
272 * A1 = Digest-User-Name ":" Realm ":" Password
274 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_USER_NAME, 0, TAG_ANY);
276 REDEBUG("No Digest-User-Name: Cannot perform Digest authentication");
277 return RLM_MODULE_INVALID;
279 memcpy(&a1[0], vp->vp_octets, vp->vp_length);
280 a1_len = vp->vp_length;
285 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_REALM, 0, TAG_ANY);
287 REDEBUG("No Digest-Realm: Cannot perform Digest authentication");
288 return RLM_MODULE_INVALID;
290 memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length);
291 a1_len += vp->vp_length;
296 if (passwd->da->attr == PW_CLEARTEXT_PASSWORD) {
297 memcpy(&a1[a1_len], passwd->vp_octets, passwd->vp_length);
298 a1_len += passwd->vp_length;
300 RDEBUG2("A1 = %s", a1);
303 RDEBUG2("A1 = %s (using Digest-HA1)", a1);
308 * See which variant we calculate.
309 * Assume MD5 if no Digest-Algorithm attribute received
311 algo = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ALGORITHM, 0, TAG_ANY);
313 (strcasecmp(algo->vp_strvalue, "MD5") == 0)) {
315 * Set A1 to Digest-HA1 if no User-Password found
317 if (passwd->da->attr == PW_DIGEST_HA1) {
318 if (fr_hex2bin(&a1[0], sizeof(a1), passwd->vp_strvalue, passwd->vp_length) != 16) {
319 RDEBUG2("Invalid text in Digest-HA1");
320 return RLM_MODULE_INVALID;
324 } else if (strcasecmp(algo->vp_strvalue, "MD5-sess") == 0) {
326 * K1 = H(A1) : Digest-Nonce ... : H(A2)
328 * If we find Digest-HA1, we assume it contains
331 if (passwd->da->attr == PW_CLEARTEXT_PASSWORD) {
332 fr_md5_calc(hash, &a1[0], a1_len);
333 fr_bin2hex((char *) &a1[0], hash, 16);
334 } else { /* MUST be Digest-HA1 */
335 memcpy(&a1[0], passwd->vp_strvalue, 32);
343 * Tack on the Digest-Nonce. Length must be even
345 if ((nonce->vp_length & 1) != 0) {
346 REDEBUG("Received Digest-Nonce hex string with invalid length: Cannot perform Digest authentication");
347 return RLM_MODULE_INVALID;
349 memcpy(&a1[a1_len], nonce->vp_octets, nonce->vp_length);
350 a1_len += nonce->vp_length;
355 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY);
357 REDEBUG("No Digest-CNonce: Cannot perform Digest authentication");
358 return RLM_MODULE_INVALID;
362 * Digest-CNonce length must be even
364 if ((vp->vp_length & 1) != 0) {
365 REDEBUG("Received Digest-CNonce hex string with invalid length: Cannot perform Digest authentication");
366 return RLM_MODULE_INVALID;
368 memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length);
369 a1_len += vp->vp_length;
371 } else if (strcasecmp(algo->vp_strvalue, "MD5") != 0) {
373 * We check for "MD5-sess" and "MD5".
374 * Anything else is an error.
376 REDEBUG("Unknown Digest-Algorithm \"%s\": Cannot perform Digest authentication", vp->vp_strvalue);
377 return RLM_MODULE_INVALID;
381 * A2 = Digest-Method ":" Digest-URI
383 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_METHOD, 0, TAG_ANY);
385 REDEBUG("No Digest-Method: Cannot perform Digest authentication");
386 return RLM_MODULE_INVALID;
388 memcpy(&a2[0], vp->vp_octets, vp->vp_length);
389 a2_len = vp->vp_length;
394 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_URI, 0, TAG_ANY);
396 REDEBUG("No Digest-URI: Cannot perform Digest authentication");
397 return RLM_MODULE_INVALID;
399 memcpy(&a2[a2_len], vp->vp_octets, vp->vp_length);
400 a2_len += vp->vp_length;
403 * QOP is "auth-int", tack on ": Digest-Body-Digest"
405 qop = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_QOP, 0, TAG_ANY);
407 if (strcasecmp(qop->vp_strvalue, "auth-int") == 0) {
411 * Add in Digest-Body-Digest
417 * Must be a hex representation of an MD5 digest.
419 body = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_BODY_DIGEST, 0, TAG_ANY);
421 REDEBUG("No Digest-Body-Digest: Cannot perform Digest authentication");
422 return RLM_MODULE_INVALID;
425 if ((a2_len + body->vp_length) > sizeof(a2)) {
426 REDEBUG("Digest-Body-Digest is too long");
427 return RLM_MODULE_INVALID;
430 memcpy(a2 + a2_len, body->vp_octets, body->vp_length);
431 a2_len += body->vp_length;
433 } else if (strcasecmp(qop->vp_strvalue, "auth") != 0) {
434 REDEBUG("Unknown Digest-QOP \"%s\": Cannot perform Digest authentication", qop->vp_strvalue);
435 return RLM_MODULE_INVALID;
440 RDEBUG2("A2 = %s", a2);
443 * KD = H(A1) : Digest-Nonce ... : H(A2).
444 * Compute MD5 if Digest-Algorithm == "MD5-Sess",
445 * or if we found a User-Password.
447 if (((algo != NULL) &&
448 (strcasecmp(algo->vp_strvalue, "MD5-Sess") == 0)) ||
449 (passwd->da->attr == PW_CLEARTEXT_PASSWORD)) {
451 fr_md5_calc(&hash[0], &a1[0], a1_len);
453 memcpy(&hash[0], &a1[0], a1_len);
455 fr_bin2hex((char *) kd, hash, sizeof(hash));
458 if (rad_debug_lvl > 1) {
459 fr_printf_log("H(A1) = ");
460 for (i = 0; i < 16; i++) {
461 fr_printf_log("%02x", hash[i]);
471 memcpy(&kd[kd_len], nonce->vp_octets, nonce->vp_length);
472 kd_len += nonce->vp_length;
475 * No QOP defined. Do RFC 2069 compatibility.
482 } else { /* Digest-QOP MUST be "auth" or "auth-int" */
484 * Tack on ":" Digest-Nonce-Count ":" Digest-CNonce
490 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE_COUNT, 0, TAG_ANY);
492 REDEBUG("No Digest-Nonce-Count: Cannot perform Digest authentication");
493 return RLM_MODULE_INVALID;
495 memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length);
496 kd_len += vp->vp_length;
501 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY);
503 REDEBUG("No Digest-CNonce: Cannot perform Digest authentication");
504 return RLM_MODULE_INVALID;
506 memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length);
507 kd_len += vp->vp_length;
512 memcpy(&kd[kd_len], qop->vp_octets, qop->vp_length);
513 kd_len += qop->vp_length;
522 fr_md5_calc(&hash[0], &a2[0], a2_len);
524 fr_bin2hex((char *) kd + kd_len, hash, sizeof(hash));
527 if (rad_debug_lvl > 1) {
528 fr_printf_log("H(A2) = ");
529 for (i = 0; i < 16; i++) {
530 fr_printf_log("%02x", hash[i]);
539 RDEBUG2("KD = %s\n", &kd[0]);
542 * Take the hash of KD.
544 fr_md5_calc(&hash[0], &kd[0], kd_len);
545 memcpy(&kd[0], &hash[0], 16);
548 * Get the binary value of Digest-Response
550 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY);
552 REDEBUG("No Digest-Response attribute in the request. Cannot perform digest authentication");
553 return RLM_MODULE_INVALID;
556 if (fr_hex2bin(&hash[0], sizeof(hash), vp->vp_strvalue, vp->vp_length) != (vp->vp_length >> 1)) {
557 RDEBUG2("Invalid text in Digest-Response");
558 return RLM_MODULE_INVALID;
562 if (rad_debug_lvl > 1) {
563 fr_printf_log("EXPECTED ");
564 for (i = 0; i < 16; i++) {
565 fr_printf_log("%02x", kd[i]);
569 fr_printf_log("RECEIVED ");
570 for (i = 0; i < 16; i++) {
571 fr_printf_log("%02x", hash[i]);
578 * And finally, compare the digest in the packet with KD.
580 if (memcmp(&kd[0], &hash[0], 16) == 0) {
581 return RLM_MODULE_OK;
584 RDEBUG("FAILED authentication");
585 return RLM_MODULE_REJECT;
589 * The module name should be the only globally exported symbol.
590 * That is, everything else should be 'static'.
592 * If the module needs to temporarily modify it's instantiation
593 * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
594 * The server will then take care of ensuring that the module
595 * is single-threaded.
597 extern module_t rlm_digest;
598 module_t rlm_digest = {
599 .magic = RLM_MODULE_INIT,
602 [MOD_AUTHENTICATE] = mod_authenticate,
603 [MOD_AUTHORIZE] = mod_authorize