Remove redundant file from freeradius-abfab list.
[freeradius.git] / src / modules / rlm_digest / rlm_digest.c
1 /*
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.
6  *
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.
11  *
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
15  */
16
17 /**
18  * $Id$
19  * @file rlm_digest.c
20  * @brief Handles SIP digest authentication requests from Cisco SIP servers.
21  *
22  * @copyright 2002,2006  The FreeRADIUS server project
23  * @copyright 2002  Alan DeKok <aland@ox.org>
24  */
25 RCSID("$Id$")
26
27 #include <freeradius-devel/radiusd.h>
28 #include <freeradius-devel/modules.h>
29 #include <freeradius-devel/md5.h>
30
31 static int digest_fix(REQUEST *request)
32 {
33         VALUE_PAIR *first, *i;
34         vp_cursor_t cursor;
35
36         /*
37          *      We need both of these attributes to do the authentication.
38          */
39         first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY);
40         if (!first) {
41                 return RLM_MODULE_NOOP;
42         }
43
44         /*
45          *      Check the sanity of the attribute.
46          */
47         if (first->vp_length != 32) {
48                 return RLM_MODULE_NOOP;
49         }
50
51         /*
52          *      Check for proper format of the Digest-Attributes
53          */
54         RDEBUG("Checking for correctly formatted Digest-Attributes");
55
56         first = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY);
57         if (!first) {
58                 return RLM_MODULE_NOOP;
59         }
60
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;
64                 int attrlen;
65                 uint8_t const *p = i->vp_octets;
66
67                 /*
68                  *      Until this stupidly encoded attribute is exhausted.
69                  */
70                 while (length > 0) {
71                         /*
72                          *      The attribute type must be valid
73                          */
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;
77                         }
78
79                         attrlen = p[1]; /* stupid VSA format */
80
81                         /*
82                          *      Too short.
83                          */
84                         if (attrlen < 3) {
85                                 RDEBUG("Not formatted as Digest-Attributes: TLV too short");
86                                 return RLM_MODULE_NOOP;
87                         }
88
89                         /*
90                          *      Too long.
91                          */
92                         if (attrlen > length) {
93                                 RDEBUG("Not formatted as Digest-Attributes: TLV too long)");
94                                 return RLM_MODULE_NOOP;
95                         }
96
97                         length -= attrlen;
98                         p += attrlen;
99                 } /* loop over this one attribute */
100         }
101
102         /*
103          *      Convert them to something sane.
104          */
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;
109                 int attrlen;
110                 uint8_t const *p = &i->vp_octets[0];
111                 char *q;
112                 VALUE_PAIR *sub;
113
114                 /*
115                  *      Until this stupidly encoded attribute is exhausted.
116                  */
117                 while (length > 0) {
118                         /*
119                          *      The attribute type must be valid
120                          */
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;
124                         }
125
126                         attrlen = p[1]; /* stupid VSA format */
127
128                         /*
129                          *      Too short.
130                          */
131                         if (attrlen < 3) {
132                                 REDEBUG("Received Digest-Attributes with short sub-attribute %d, of length %d", p[0], attrlen);
133                                 return RLM_MODULE_INVALID;
134                         }
135
136                         /*
137                          *      Too long.
138                          */
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;
142                         }
143
144                         /*
145                          *      Create a new attribute, broken out of
146                          *      the stupid sub-attribute crap.
147                          *
148                          *      Didn't they know that VSA's exist?
149                          */
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';
156
157                         if ((rad_debug_lvl > 1) && fr_log_fp) {
158                                 vp_print(fr_log_fp, sub);
159                         }
160
161                         /*
162                          *      FIXME: Check for the existence
163                          *      of the necessary attributes!
164                          */
165
166                         length -= attrlen;
167                         p += attrlen;
168                 } /* loop over this one attribute */
169         }
170
171         return RLM_MODULE_OK;
172 }
173
174 static rlm_rcode_t CC_HINT(nonnull) mod_authorize(UNUSED void *instance, REQUEST *request)
175 {
176         rlm_rcode_t rcode;
177
178         /*
179          *      Double-check and fix the attributes.
180          */
181         rcode = digest_fix(request);
182         if (rcode != RLM_MODULE_OK) return rcode;
183
184
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;
188         }
189
190         /*
191          *      Everything's OK, add a digest authentication type.
192          */
193         RDEBUG("Adding Auth-Type = DIGEST");
194         pair_make_config("Auth-Type", "DIGEST", T_OP_EQ);
195
196         return RLM_MODULE_OK;
197 }
198
199 /*
200  *      Perform all of the wondrous variants of digest authentication.
201  */
202 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request)
203 {
204         int i;
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;
212
213         /*
214          *      We require access to the plain-text password, or to the
215          *      Digest-HA1 parameter.
216          */
217         passwd = fr_pair_find_by_num(request->config, PW_DIGEST_HA1, 0, TAG_ANY);
218         if (passwd) {
219                 if (passwd->vp_length != 32) {
220                         RAUTH("Digest-HA1 has invalid length, authentication failed");
221                         return RLM_MODULE_INVALID;
222                 }
223         } else {
224                 passwd = fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY);
225         }
226         if (!passwd) {
227                 RAUTH("Cleartext-Password or Digest-HA1 is required for authentication");
228                 return RLM_MODULE_INVALID;
229         }
230
231         /*
232          *      We need these, too.
233          */
234         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0, TAG_ANY);
235         if (!vp) {
236         error:
237                 REDEBUG("You set 'Auth-Type = Digest' for a request that does not contain any digest attributes!");
238                 return RLM_MODULE_INVALID;
239         }
240
241         /*
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
246          *      attributes here.
247          */
248         if (!fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY)) {
249                 int rcode;
250
251                 rcode = digest_fix(request);
252
253                 /*
254                  *      NOOP means "couldn't find the attributes".
255                  *      That's bad.
256                  */
257                 if (rcode == RLM_MODULE_NOOP) goto error;
258
259                 if (rcode != RLM_MODULE_OK) return rcode;
260         }
261
262         /*
263          *      We require access to the Digest-Nonce-Value
264          */
265         nonce = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE, 0, TAG_ANY);
266         if (!nonce) {
267                 REDEBUG("No Digest-Nonce: Cannot perform Digest authentication");
268                 return RLM_MODULE_INVALID;
269         }
270
271         /*
272          *      A1 = Digest-User-Name ":" Realm ":" Password
273          */
274         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_USER_NAME, 0, TAG_ANY);
275         if (!vp) {
276                 REDEBUG("No Digest-User-Name: Cannot perform Digest authentication");
277                 return RLM_MODULE_INVALID;
278         }
279         memcpy(&a1[0], vp->vp_octets, vp->vp_length);
280         a1_len = vp->vp_length;
281
282         a1[a1_len] = ':';
283         a1_len++;
284
285         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_REALM, 0, TAG_ANY);
286         if (!vp) {
287                 REDEBUG("No Digest-Realm: Cannot perform Digest authentication");
288                 return RLM_MODULE_INVALID;
289         }
290         memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length);
291         a1_len += vp->vp_length;
292
293         a1[a1_len] = ':';
294         a1_len++;
295
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;
299                 a1[a1_len] = '\0';
300                 RDEBUG2("A1 = %s", a1);
301         } else {
302                 a1[a1_len] = '\0';
303                 RDEBUG2("A1 = %s (using Digest-HA1)", a1);
304                 a1_len = 16;
305         }
306
307         /*
308          *      See which variant we calculate.
309          *      Assume MD5 if no Digest-Algorithm attribute received
310          */
311         algo = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_ALGORITHM, 0, TAG_ANY);
312         if ((!algo) ||
313             (strcasecmp(algo->vp_strvalue, "MD5") == 0)) {
314                 /*
315                  *      Set A1 to Digest-HA1 if no User-Password found
316                  */
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;
321                         }
322                 }
323
324         } else if (strcasecmp(algo->vp_strvalue, "MD5-sess") == 0) {
325                 /*
326                  *      K1 = H(A1) : Digest-Nonce ... : H(A2)
327                  *
328                  *      If we find Digest-HA1, we assume it contains
329                  *      H(A1).
330                  */
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);
336                 }
337                 a1_len = 32;
338
339                 a1[a1_len] = ':';
340                 a1_len++;
341
342                 /*
343                  *      Tack on the Digest-Nonce. Length must be even
344                  */
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;
348                 }
349                 memcpy(&a1[a1_len], nonce->vp_octets, nonce->vp_length);
350                 a1_len += nonce->vp_length;
351
352                 a1[a1_len] = ':';
353                 a1_len++;
354
355                 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY);
356                 if (!vp) {
357                         REDEBUG("No Digest-CNonce: Cannot perform Digest authentication");
358                         return RLM_MODULE_INVALID;
359                 }
360
361                 /*
362                  *      Digest-CNonce length must be even
363                  */
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;
367                 }
368                 memcpy(&a1[a1_len], vp->vp_octets, vp->vp_length);
369                 a1_len += vp->vp_length;
370
371         } else if (strcasecmp(algo->vp_strvalue, "MD5") != 0) {
372                 /*
373                  *      We check for "MD5-sess" and "MD5".
374                  *      Anything else is an error.
375                  */
376                 REDEBUG("Unknown Digest-Algorithm \"%s\": Cannot perform Digest authentication", vp->vp_strvalue);
377                 return RLM_MODULE_INVALID;
378         }
379
380         /*
381          *      A2 = Digest-Method ":" Digest-URI
382          */
383         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_METHOD, 0, TAG_ANY);
384         if (!vp) {
385                 REDEBUG("No Digest-Method: Cannot perform Digest authentication");
386                 return RLM_MODULE_INVALID;
387         }
388         memcpy(&a2[0], vp->vp_octets, vp->vp_length);
389         a2_len = vp->vp_length;
390
391         a2[a2_len] = ':';
392         a2_len++;
393
394         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_URI, 0, TAG_ANY);
395         if (!vp) {
396                 REDEBUG("No Digest-URI: Cannot perform Digest authentication");
397                 return RLM_MODULE_INVALID;
398         }
399         memcpy(&a2[a2_len], vp->vp_octets, vp->vp_length);
400         a2_len += vp->vp_length;
401
402         /*
403          *  QOP is "auth-int", tack on ": Digest-Body-Digest"
404          */
405         qop = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_QOP, 0, TAG_ANY);
406         if (qop) {
407                 if (strcasecmp(qop->vp_strvalue, "auth-int") == 0) {
408                         VALUE_PAIR *body;
409
410                         /*
411                          *      Add in Digest-Body-Digest
412                          */
413                         a2[a2_len] = ':';
414                         a2_len++;
415
416                         /*
417                          *  Must be a hex representation of an MD5 digest.
418                          */
419                         body = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_BODY_DIGEST, 0, TAG_ANY);
420                         if (!body) {
421                                 REDEBUG("No Digest-Body-Digest: Cannot perform Digest authentication");
422                                 return RLM_MODULE_INVALID;
423                         }
424
425                         if ((a2_len + body->vp_length) > sizeof(a2)) {
426                                 REDEBUG("Digest-Body-Digest is too long");
427                                 return RLM_MODULE_INVALID;
428                         }
429
430                         memcpy(a2 + a2_len, body->vp_octets, body->vp_length);
431                         a2_len += body->vp_length;
432
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;
436                 }
437         }
438
439         a2[a2_len] = '\0';
440         RDEBUG2("A2 = %s", a2);
441
442         /*
443          *     KD = H(A1) : Digest-Nonce ... : H(A2).
444          *     Compute MD5 if Digest-Algorithm == "MD5-Sess",
445          *     or if we found a User-Password.
446          */
447         if (((algo != NULL) &&
448              (strcasecmp(algo->vp_strvalue, "MD5-Sess") == 0)) ||
449             (passwd->da->attr == PW_CLEARTEXT_PASSWORD)) {
450                 a1[a1_len] = '\0';
451                 fr_md5_calc(&hash[0], &a1[0], a1_len);
452         } else {
453                 memcpy(&hash[0], &a1[0], a1_len);
454         }
455         fr_bin2hex((char *) kd, hash, sizeof(hash));
456
457 #ifndef NRDEBUG
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]);
462                 }
463                 fr_printf_log("\n");
464         }
465 #endif
466         kd_len = 32;
467
468         kd[kd_len] = ':';
469         kd_len++;
470
471         memcpy(&kd[kd_len], nonce->vp_octets, nonce->vp_length);
472         kd_len += nonce->vp_length;
473
474         /*
475          *      No QOP defined.  Do RFC 2069 compatibility.
476          */
477         if (!qop) {
478                 /*
479                  *      Do nothing here.
480                  */
481
482         } else {                /* Digest-QOP MUST be "auth" or "auth-int" */
483                 /*
484                  *      Tack on ":" Digest-Nonce-Count ":" Digest-CNonce
485                  *             ":" Digest-QOP
486                  */
487                 kd[kd_len] = ':';
488                 kd_len++;
489
490                 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_NONCE_COUNT, 0, TAG_ANY);
491                 if (!vp) {
492                         REDEBUG("No Digest-Nonce-Count: Cannot perform Digest authentication");
493                         return RLM_MODULE_INVALID;
494                 }
495                 memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length);
496                 kd_len += vp->vp_length;
497
498                 kd[kd_len] = ':';
499                 kd_len++;
500
501                 vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_CNONCE, 0, TAG_ANY);
502                 if (!vp) {
503                         REDEBUG("No Digest-CNonce: Cannot perform Digest authentication");
504                         return RLM_MODULE_INVALID;
505                 }
506                 memcpy(&kd[kd_len], vp->vp_octets, vp->vp_length);
507                 kd_len += vp->vp_length;
508
509                 kd[kd_len] = ':';
510                 kd_len++;
511
512                 memcpy(&kd[kd_len], qop->vp_octets, qop->vp_length);
513                 kd_len += qop->vp_length;
514         }
515
516         /*
517          *      Tack on ":" H(A2)
518          */
519         kd[kd_len] = ':';
520         kd_len++;
521
522         fr_md5_calc(&hash[0], &a2[0], a2_len);
523
524         fr_bin2hex((char *) kd + kd_len, hash, sizeof(hash));
525
526 #ifndef NRDEBUG
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]);
531                 }
532                 fr_printf_log("\n");
533         }
534 #endif
535         kd_len += 32;
536
537         kd[kd_len] = 0;
538
539         RDEBUG2("KD = %s\n", &kd[0]);
540
541         /*
542          *      Take the hash of KD.
543          */
544         fr_md5_calc(&hash[0], &kd[0], kd_len);
545         memcpy(&kd[0], &hash[0], 16);
546
547         /*
548          *      Get the binary value of Digest-Response
549          */
550         vp = fr_pair_find_by_num(request->packet->vps, PW_DIGEST_RESPONSE, 0, TAG_ANY);
551         if (!vp) {
552                 REDEBUG("No Digest-Response attribute in the request.  Cannot perform digest authentication");
553                 return RLM_MODULE_INVALID;
554         }
555
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;
559         }
560
561 #ifndef NRDEBUG
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]);
566                 }
567                 fr_printf_log("\n");
568
569                 fr_printf_log("RECEIVED ");
570                 for (i = 0; i < 16; i++) {
571                         fr_printf_log("%02x", hash[i]);
572                 }
573                 fr_printf_log("\n");
574         }
575 #endif
576
577         /*
578          *  And finally, compare the digest in the packet with KD.
579          */
580         if (memcmp(&kd[0], &hash[0], 16) == 0) {
581                 return RLM_MODULE_OK;
582         }
583
584         RDEBUG("FAILED authentication");
585         return RLM_MODULE_REJECT;
586 }
587
588 /*
589  *      The module name should be the only globally exported symbol.
590  *      That is, everything else should be 'static'.
591  *
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.
596  */
597 extern module_t rlm_digest;
598 module_t rlm_digest = {
599         .magic          = RLM_MODULE_INIT,
600         .name           = "digest",
601         .methods = {
602                 [MOD_AUTHENTICATE]      = mod_authenticate,
603                 [MOD_AUTHORIZE]         = mod_authorize
604         },
605 };