Be moe forgiving about the Digest attributes.
[freeradius.git] / src / modules / rlm_digest / rlm_digest.c
1 /*
2  * rlm_chap.c
3  *
4  * Version:  $Id$
5  *
6  *   This program is free software; you can redistribute it and/or modify
7  *   it under the terms of the GNU General Public License as published by
8  *   the Free Software Foundation; either version 2 of the License, or
9  *   (at your option) any later version.
10  *
11  *   This program is distributed in the hope that it will be useful,
12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *   GNU General Public License for more details.
15  *
16  *   You should have received a copy of the GNU General Public License
17  *   along with this program; if not, write to the Free Software
18  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19  *
20  * Copyright 2002,2006  The FreeRADIUS server project
21  * Copyright 2002  Alan DeKok <aland@ox.org>
22  */
23
24 #include <freeradius-devel/ident.h>
25 RCSID("$Id$")
26
27 #include <freeradius-devel/radiusd.h>
28 #include <freeradius-devel/modules.h>
29
30 static int digest_fix(REQUEST *request)
31 {
32         VALUE_PAIR *vp;
33
34         /*
35          *      We need both of these attributes to do the authentication.
36          */
37         vp = pairfind(request->packet->vps, PW_DIGEST_RESPONSE, 0);
38         if (vp == NULL) {
39                 return RLM_MODULE_NOOP;
40         }
41
42         /*
43          *      Check the sanity of the attribute.
44          */
45         if (vp->length != 32) {
46                 return RLM_MODULE_NOOP;
47         }
48
49         /*
50          *      We need these, too.
51          */
52         vp = pairfind(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0);
53         if (vp == NULL) {
54                 return RLM_MODULE_NOOP;
55         }
56
57         /*
58          *      Check for proper format of the Digest-Attributes
59          */
60         RDEBUG("Checking for correctly formatted Digest-Attributes");
61         while (vp) {
62                 int length = vp->length;
63                 int attrlen;
64                 uint8_t *p = &vp->vp_octets[0];
65
66                 /*
67                  *      Until this stupidly encoded attribute is exhausted.
68                  */
69                 while (length > 0) {
70                         /*
71                          *      The attribute type must be valid
72                          */
73                         if ((p[0] == 0) || (p[0] > 10)) {
74                                 RDEBUG("Not formatted as Digest-Attributes");
75                                 return RLM_MODULE_NOOP;
76                         }
77
78                         attrlen = p[1]; /* stupid VSA format */
79
80                         /*
81                          *      Too short.
82                          */
83                         if (attrlen < 3) {
84                                 RDEBUG("Not formatted as Digest-Attributes");
85                                 return RLM_MODULE_NOOP;
86                         }
87
88                         /*
89                          *      Too long.
90                          */
91                         if (attrlen > length) {
92                                 RDEBUG("Not formatted as Digest-Attributes");
93                                 return RLM_MODULE_NOOP;
94                         }
95
96                         length -= attrlen;
97                         p += attrlen;
98                 } /* loop over this one attribute */
99
100                 /*
101                  *      Find the next one, if it exists.
102                  */
103                 vp = pairfind(vp->next, PW_DIGEST_ATTRIBUTES, 0);
104         }
105
106         /*
107          *      Convert them to something sane.
108          */
109         RDEBUG("Digest-Attributes look OK.  Converting them to something more usful.");
110         vp = pairfind(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0);
111         while (vp) {
112                 int length = vp->length;
113                 int attrlen;
114                 uint8_t *p = &vp->vp_octets[0];
115                 VALUE_PAIR *sub;
116
117                 /*
118                  *      Until this stupidly encoded attribute is exhausted.
119                  */
120                 while (length > 0) {
121                         /*
122                          *      The attribute type must be valid
123                          */
124                         if ((p[0] == 0) || (p[0] > 10)) {
125                                 RDEBUG("ERROR: Received Digest-Attributes with invalid sub-attribute %d", p[0]);
126                                 return RLM_MODULE_INVALID;
127                         }
128
129                         attrlen = p[1]; /* stupid VSA format */
130
131                         /*
132                          *      Too short.
133                          */
134                         if (attrlen < 3) {
135                                 RDEBUG("ERROR: Received Digest-Attributes with short sub-attribute %d, of length %d", p[0], attrlen);
136                                 return RLM_MODULE_INVALID;
137                         }
138
139                         /*
140                          *      Too long.
141                          */
142                         if (attrlen > length) {
143                                 RDEBUG("ERROR: Received Digest-Attributes with long sub-attribute %d, of length %d", p[0], attrlen);
144                                 return RLM_MODULE_INVALID;
145                         }
146
147                         /*
148                          *      Create a new attribute, broken out of
149                          *      the stupid sub-attribute crap.
150                          *
151                          *      Didn't they know that VSA's exist?
152                          */
153                         sub = radius_paircreate(request, &request->packet->vps,
154                                                 PW_DIGEST_REALM - 1 + p[0], 0,
155                                                 PW_TYPE_STRING);
156                         memcpy(&sub->vp_octets[0], &p[2], attrlen - 2);
157                         sub->vp_octets[attrlen - 2] = '\0';
158                         sub->length = attrlen - 2;
159
160                         if ((debug_flag > 1) && fr_log_fp) {
161                           fputc('\t', fr_log_fp);
162                           vp_print(fr_log_fp, sub);
163                           fputc('\n', fr_log_fp);
164                         }
165
166                         /*
167                          *      FIXME: Check for the existence
168                          *      of the necessary attributes!
169                          */
170
171                         length -= attrlen;
172                         p += attrlen;
173                 } /* loop over this one attribute */
174
175                 /*
176                  *      Find the next one, if it exists.
177                  */
178                 vp = pairfind(vp->next, PW_DIGEST_ATTRIBUTES, 0);
179         }
180
181         return RLM_MODULE_OK;
182 }
183
184 static int digest_authorize(void *instance, REQUEST *request)
185 {
186         int rcode;
187
188         /* quiet the compiler */
189         instance = instance;
190
191         /*
192          *      Double-check and fix the attributes.
193          */       
194         rcode = digest_fix(request);
195         if (rcode != RLM_MODULE_OK) return rcode;
196
197         /*
198          *      Everything's OK, add a digest authentication type.
199          */
200         if (pairfind(request->config_items, PW_AUTHTYPE, 0) == NULL) {
201                 RDEBUG("Adding Auth-Type = DIGEST");
202                 pairadd(&request->config_items,
203                         pairmake("Auth-Type", "DIGEST", T_OP_EQ));
204         }
205
206         return RLM_MODULE_OK;
207 }
208
209 /*
210  *      Perform all of the wondrous variants of digest authentication.
211  */
212 static int digest_authenticate(void *instance, REQUEST *request)
213 {
214         int i;
215         size_t a1_len, a2_len, kd_len;
216         uint8_t a1[(MAX_STRING_LEN + 1) * 5]; /* can be 5 attributes */
217         uint8_t a2[(MAX_STRING_LEN + 1) * 3]; /* can be 3 attributes */
218         uint8_t kd[(MAX_STRING_LEN + 1) * 5];
219         uint8_t hash[16];       /* MD5 output */
220         VALUE_PAIR *vp, *passwd, *algo;
221         VALUE_PAIR *qop, *nonce;
222
223         instance = instance;    /* -Wunused */
224
225         /*
226          *      We require access to the plain-text password, or to the
227          *      Digest-HA1 parameter.
228          */
229         passwd = pairfind(request->config_items, PW_DIGEST_HA1, 0);
230         if (passwd) {
231                 if (passwd->length != 32) {
232                         radlog_request(L_AUTH, 0, request, "Digest-HA1 has invalid length, authentication failed.");
233                         return RLM_MODULE_INVALID;
234                 }
235         } else {
236                 passwd = pairfind(request->config_items, PW_CLEARTEXT_PASSWORD, 0);
237         }
238         if (!passwd) {
239                 radlog_request(L_AUTH, 0, request, "Cleartext-Password or Digest-HA1 is required for authentication.");
240                 return RLM_MODULE_INVALID;
241         }
242
243         /*
244          *      We need these, too.
245          */
246         vp = pairfind(request->packet->vps, PW_DIGEST_ATTRIBUTES, 0);
247         if (vp == NULL) {
248         error:
249                 RDEBUG("ERROR: You set 'Auth-Type = Digest' for a request that did not contain any digest attributes!w");
250                 return RLM_MODULE_INVALID;
251         }
252
253         /*
254          *      Look for the "internal" FreeRADIUS Digest attributes.
255          *      If they don't exist, it means that someone forced
256          *      Auth-Type = digest, without putting "digest" into the
257          *      "authorize" section.  In that case, try to decode the
258          *      attributes here.
259          */
260         if (!pairfind(request->packet->vps, PW_DIGEST_NONCE, 0)) {
261                 int rcode;
262
263                 rcode = digest_fix(request);
264
265                 /*
266                  *      NOOP means "couldn't find the attributes".
267                  *      That's bad.
268                  */
269                 if (rcode == RLM_MODULE_NOOP) goto error;
270
271                 if (rcode != RLM_MODULE_OK) return rcode;
272         }
273
274         /*
275          *      We require access to the Digest-Nonce-Value
276          */
277         nonce = pairfind(request->packet->vps, PW_DIGEST_NONCE, 0);
278         if (!nonce) {
279                 RDEBUG("ERROR: No Digest-Nonce: Cannot perform Digest authentication");
280                 return RLM_MODULE_INVALID;
281         }
282
283         /*
284          *      A1 = Digest-User-Name ":" Realm ":" Password
285          */
286         vp = pairfind(request->packet->vps, PW_DIGEST_USER_NAME, 0);
287         if (!vp) {
288                 RDEBUG("ERROR: No Digest-User-Name: Cannot perform Digest authentication");
289                 return RLM_MODULE_INVALID;
290         }
291         memcpy(&a1[0], &vp->vp_octets[0], vp->length);
292         a1_len = vp->length;
293
294         a1[a1_len] = ':';
295         a1_len++;
296
297         vp = pairfind(request->packet->vps, PW_DIGEST_REALM, 0);
298         if (!vp) {
299                 RDEBUG("ERROR: No Digest-Realm: Cannot perform Digest authentication");
300                 return RLM_MODULE_INVALID;
301         }
302         memcpy(&a1[a1_len], &vp->vp_octets[0], vp->length);
303         a1_len += vp->length;
304
305         a1[a1_len] = ':';
306         a1_len++;
307
308         if (passwd->attribute == PW_CLEARTEXT_PASSWORD) {
309                 memcpy(&a1[a1_len], &passwd->vp_octets[0], passwd->length);
310                 a1_len += passwd->length;
311                 a1[a1_len] = '\0';
312                 RDEBUG2("A1 = %s", a1);
313         } else {
314                 a1[a1_len] = '\0';
315                 RDEBUG2("A1 = %s (using Digest-HA1)", a1);
316                 a1_len = 16;
317         }
318
319         /*
320          *      See which variant we calculate.
321          *      Assume MD5 if no Digest-Algorithm attribute received
322          */
323         algo = pairfind(request->packet->vps, PW_DIGEST_ALGORITHM, 0);
324         if ((algo == NULL) ||
325             (strcasecmp(algo->vp_strvalue, "MD5") == 0)) {
326                 /*
327                  *      Set A1 to Digest-HA1 if no User-Password found
328                  */
329                 if (passwd->attribute == PW_DIGEST_HA1) {
330                         if (fr_hex2bin(passwd->vp_strvalue, &a1[0], 16) != 16) {
331                                 RDEBUG2("Invalid text in Digest-HA1");
332                                 return RLM_MODULE_INVALID;
333                         }
334                 }
335
336         } else if (strcasecmp(algo->vp_strvalue, "MD5-sess") == 0) {
337                 /*
338                  *      K1 = H(A1) : Digest-Nonce ... : H(A2)
339                  *
340                  *      If we find Digest-HA1, we assume it contains
341                  *      H(A1).
342                  */
343                 if (passwd->attribute == PW_CLEARTEXT_PASSWORD) {
344                         fr_md5_calc(hash, &a1[0], a1_len);
345                         fr_bin2hex(hash, (char *) &a1[0], 16);
346                 } else {        /* MUST be Digest-HA1 */
347                         memcpy(&a1[0], passwd->vp_strvalue, 32);
348                 }
349                 a1_len = 32;
350
351                 a1[a1_len] = ':';
352                 a1_len++;
353
354                 /*
355                  *      Tack on the Digest-Nonce. Length must be even
356                  */
357                 if ((nonce->length & 1) != 0) {
358                         RDEBUG("ERROR: Received Digest-Nonce hex string with invalid length: Cannot perform Digest authentication");
359                         return RLM_MODULE_INVALID;
360                 }
361                 memcpy(&a1[a1_len], &nonce->vp_octets[0], nonce->length);
362                 a1_len += nonce->length;
363
364                 a1[a1_len] = ':';
365                 a1_len++;
366
367                 vp = pairfind(request->packet->vps, PW_DIGEST_CNONCE, 0);
368                 if (!vp) {
369                         RDEBUG("ERROR: No Digest-CNonce: Cannot perform Digest authentication");
370                         return RLM_MODULE_INVALID;
371                 }
372
373                 /*
374                  *      Digest-CNonce length must be even
375                  */
376                 if ((vp->length & 1) != 0) {
377                         RDEBUG("ERROR: Received Digest-CNonce hex string with invalid length: Cannot perform Digest authentication");
378                         return RLM_MODULE_INVALID;
379                 }
380                 memcpy(&a1[a1_len], &vp->vp_octets[0], vp->length);
381                 a1_len += vp->length;
382
383         } else if ((algo != NULL) &&
384                    (strcasecmp(algo->vp_strvalue, "MD5") != 0)) {
385                 /*
386                  *      We check for "MD5-sess" and "MD5".
387                  *      Anything else is an error.
388                  */
389                 RDEBUG("ERROR: Unknown Digest-Algorithm \"%s\": Cannot perform Digest authentication", vp->vp_strvalue);
390                 return RLM_MODULE_INVALID;
391         }
392
393         /*
394          *      A2 = Digest-Method ":" Digest-URI
395          */
396         vp = pairfind(request->packet->vps, PW_DIGEST_METHOD, 0);
397         if (!vp) {
398                 RDEBUG("ERROR: No Digest-Method: Cannot perform Digest authentication");
399                 return RLM_MODULE_INVALID;
400         }
401         memcpy(&a2[0], &vp->vp_octets[0], vp->length);
402         a2_len = vp->length;
403
404         a2[a2_len] = ':';
405         a2_len++;
406
407         vp = pairfind(request->packet->vps, PW_DIGEST_URI, 0);
408         if (!vp) {
409                 RDEBUG("ERROR: No Digest-URI: Cannot perform Digest authentication");
410                 return RLM_MODULE_INVALID;
411         }
412         memcpy(&a2[a2_len], &vp->vp_octets[0], vp->length);
413         a2_len += vp->length;
414
415         /*
416          *  QOP is "auth-int", tack on ": Digest-Body-Digest"
417          */
418         qop = pairfind(request->packet->vps, PW_DIGEST_QOP, 0);
419         if ((qop != NULL) &&
420             (strcasecmp(qop->vp_strvalue, "auth-int") == 0)) {
421                 VALUE_PAIR *body;
422
423                 /*
424                  *      Add in Digest-Body-Digest
425                  */
426                 a2[a2_len] = ':';
427                 a2_len++;
428
429                 /*
430                  *  Must be a hex representation of an MD5 digest.
431                  */
432                 body = pairfind(request->packet->vps, PW_DIGEST_BODY_DIGEST, 0);
433                 if (!body) {
434                         RDEBUG("ERROR: No Digest-Body-Digest: Cannot perform Digest authentication");
435                         return RLM_MODULE_INVALID;
436                 }
437
438                 if ((a2_len + body->length) > sizeof(a2)) {
439                         RDEBUG("ERROR: Digest-Body-Digest is too long");
440                         return RLM_MODULE_INVALID;
441                 }
442
443                 memcpy(a2 + a2_len, body->vp_octets, body->length);
444                 a2_len += body->length;
445
446         } else if ((qop != NULL) &&
447                    (strcasecmp(qop->vp_strvalue, "auth") != 0)) {
448                 RDEBUG("ERROR: Unknown Digest-QOP \"%s\": Cannot perform Digest authentication", qop->vp_strvalue);
449                 return RLM_MODULE_INVALID;
450         }
451
452         a2[a2_len] = '\0';
453         RDEBUG2("A2 = %s", a2);
454
455         /*
456          *     KD = H(A1) : Digest-Nonce ... : H(A2).
457          *     Compute MD5 if Digest-Algorithm == "MD5-Sess",
458          *     or if we found a User-Password.
459          */
460         if (((algo != NULL) &&
461              (strcasecmp(algo->vp_strvalue, "MD5-Sess") == 0)) ||
462             (passwd->attribute == PW_CLEARTEXT_PASSWORD)) {
463                 a1[a1_len] = '\0';
464                 fr_md5_calc(&hash[0], &a1[0], a1_len);
465         } else {
466                 memcpy(&hash[0], &a1[0], a1_len);
467         }
468         fr_bin2hex(hash, (char *) kd, sizeof(hash));
469
470 #ifndef NRDEBUG
471         if (debug_flag > 1) {
472                 fr_printf_log("H(A1) = ");
473                 for (i = 0; i < 16; i++) {
474                         fr_printf_log("%02x", hash[i]);
475                 }
476                 fr_printf_log("\n");
477         }
478 #endif
479         kd_len = 32;
480
481         kd[kd_len] = ':';
482         kd_len++;
483
484         memcpy(&kd[kd_len], nonce->vp_octets, nonce->length);
485         kd_len += nonce->length;
486
487         /*
488          *      No QOP defined.  Do RFC 2069 compatibility.
489          */
490         if (!qop) {
491                 /*
492                  *      Do nothing here.
493                  */
494
495         } else {                /* Digest-QOP MUST be "auth" or "auth-int" */
496                 /*
497                  *      Tack on ":" Digest-Nonce-Count ":" Digest-CNonce
498                  *             ":" Digest-QOP
499                  */
500                 kd[kd_len] = ':';
501                 kd_len++;
502
503                 vp = pairfind(request->packet->vps, PW_DIGEST_NONCE_COUNT, 0);
504                 if (!vp) {
505                         RDEBUG("ERROR: No Digest-Nonce-Count: Cannot perform Digest authentication");
506                         return RLM_MODULE_INVALID;
507                 }
508                 memcpy(&kd[kd_len], &vp->vp_octets[0], vp->length);
509                 kd_len += vp->length;
510
511                 kd[kd_len] = ':';
512                 kd_len++;
513
514                 vp = pairfind(request->packet->vps, PW_DIGEST_CNONCE, 0);
515                 if (!vp) {
516                         RDEBUG("ERROR: No Digest-CNonce: Cannot perform Digest authentication");
517                         return RLM_MODULE_INVALID;
518                 }
519                 memcpy(&kd[kd_len], &vp->vp_octets[0], vp->length);
520                 kd_len += vp->length;
521
522                 kd[kd_len] = ':';
523                 kd_len++;
524
525                 memcpy(&kd[kd_len], &qop->vp_octets[0], qop->length);
526                 kd_len += qop->length;
527         }
528
529         /*
530          *      Tack on ":" H(A2)
531          */
532         kd[kd_len] = ':';
533         kd_len++;
534
535         fr_md5_calc(&hash[0], &a2[0], a2_len);
536
537         fr_bin2hex(hash, (char *) kd + kd_len, sizeof(hash));
538
539 #ifndef NRDEBUG
540         if (debug_flag > 1) {
541                 fr_printf_log("H(A2) = ");
542                 for (i = 0; i < 16; i++) {
543                         fr_printf_log("%02x", hash[i]);
544                 }
545                 fr_printf_log("\n");
546         }
547 #endif
548         kd_len += 32;
549
550         kd[kd_len] = 0;
551
552         RDEBUG2("KD = %s\n", &kd[0]);
553
554         /*
555          *      Take the hash of KD.
556          */
557         fr_md5_calc(&hash[0], &kd[0], kd_len);
558         memcpy(&kd[0], &hash[0], 16);
559
560         /*
561          *      Get the binary value of Digest-Response
562          */
563         vp = pairfind(request->packet->vps, PW_DIGEST_RESPONSE, 0);
564         if (!vp) {
565                 RDEBUG("ERROR: No Digest-Response attribute in the request.  Cannot perform digest authentication");
566                 return RLM_MODULE_INVALID;
567         }
568
569         if (fr_hex2bin(&vp->vp_strvalue[0], &hash[0], vp->length >> 1) != (vp->length >> 1)) {
570                 RDEBUG2("Invalid text in Digest-Response");
571                 return RLM_MODULE_INVALID;
572         }
573
574 #ifndef NRDEBUG
575         if (debug_flag > 1) {
576                 fr_printf_log("EXPECTED ");
577                 for (i = 0; i < 16; i++) {
578                         fr_printf_log("%02x", kd[i]);
579                 }
580                 fr_printf_log("\n");
581
582                 fr_printf_log("RECEIVED ");
583                 for (i = 0; i < 16; i++) {
584                         fr_printf_log("%02x", hash[i]);
585                 }
586                 fr_printf_log("\n");
587         }
588 #endif
589
590         /*
591          *  And finally, compare the digest in the packet with KD.
592          */
593         if (memcmp(&kd[0], &hash[0], 16) == 0) {
594                 return RLM_MODULE_OK;
595         }
596
597         RDEBUG("FAILED authentication");
598         return RLM_MODULE_REJECT;
599 }
600
601 /*
602  *      The module name should be the only globally exported symbol.
603  *      That is, everything else should be 'static'.
604  *
605  *      If the module needs to temporarily modify it's instantiation
606  *      data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
607  *      The server will then take care of ensuring that the module
608  *      is single-threaded.
609  */
610 module_t rlm_digest = {
611         RLM_MODULE_INIT,
612         "digest",
613         RLM_TYPE_CHECK_CONFIG_SAFE,     /* type */
614         NULL,                           /* instantiation */
615         NULL,                           /* detach */
616         {
617                 digest_authenticate,    /* authentication */
618                 digest_authorize,       /* authorization */
619                 NULL,                   /* preaccounting */
620                 NULL,                   /* accounting */
621                 NULL,                   /* checksimul */
622                 NULL,                   /* pre-proxy */
623                 NULL,                   /* post-proxy */
624                 NULL                    /* post-auth */
625         },
626 };