1/* parser.c --- DIGEST-MD5 parser.
2 * Copyright (C) 2002-2012 Simon Josefsson
3 *
4 * This file is part of GNU SASL Library.
5 *
6 * GNU SASL Library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public License
8 * as published by the Free Software Foundation; either version 2.1 of
9 * the License, or (at your option) any later version.
10 *
11 * GNU SASL Library 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 GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with GNU SASL Library; if not, write to the Free
18 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
20 *
21 */
22
23#ifdef HAVE_CONFIG_H
24#include "config.h"
25#endif
26
27/* Get prototypes. */
28#include "parser.h"
29
30/* Get malloc, free. */
31#include <stdlib.h>
32
33/* Get memcpy, strlen. */
34#include <string.h>
35
36/* Get validator. */
37#include "validate.h"
38
39#define DEFAULT_CHARSET "utf-8"
40#define DEFAULT_ALGORITHM "md5-sess"
41
42enum
43{
44 /* the order must match the following struct */
45 CHALLENGE_REALM = 0,
46 CHALLENGE_NONCE,
47 CHALLENGE_QOP,
48 CHALLENGE_STALE,
49 CHALLENGE_MAXBUF,
50 CHALLENGE_CHARSET,
51 CHALLENGE_ALGORITHM,
52 CHALLENGE_CIPHER
53};
54
55static const char *const digest_challenge_opts[] = {
56 /* the order must match the previous enum */
57 "realm",
58 "nonce",
59 "qop",
60 "stale",
61 "maxbuf",
62 "charset",
63 "algorithm",
64 "cipher",
65 NULL
66};
67
68/* qop-value = "auth" | "auth-int" | "auth-conf" | qop-token */
69enum
70{
71 /* the order must match the following struct */
72 QOP_AUTH = 0,
73 QOP_AUTH_INT,
74 QOP_AUTH_CONF
75};
76
77static const char *const qop_opts[] = {
78 /* the order must match the previous enum */
79 "auth",
80 "auth-int",
81 "auth-conf",
82 NULL
83};
84
85/* cipher-value = "3des" | "des" | "rc4-40" | "rc4" |
86 * "rc4-56" | "aes-cbc" | cipher-token
87 * ;; "des" and "3des" ciphers are obsolete.
88 */
89enum
90{
91 /* the order must match the following struct */
92 CIPHER_DES = 0,
93 CIPHER_3DES,
94 CIPHER_RC4,
95 CIPHER_RC4_40,
96 CIPHER_RC4_56,
97 CIPHER_AES_CBC
98};
99
100static const char *const cipher_opts[] = {
101 /* the order must match the previous enum */
102 "des",
103 "3des",
104 "rc4",
105 "rc4-40",
106 "rc4-56",
107 "aes-cbc",
108 NULL
109};
110
111static int
112parse_challenge (char *challenge, digest_md5_challenge * out)
113{
114 int done_algorithm = 0;
115 int disable_qop_auth_conf = 0;
116 char *value;
117
118 memset (out, 0, sizeof (*out));
119
120 /* The size of a digest-challenge MUST be less than 2048 bytes. */
121 if (strlen (challenge) >= 2048)
122 return -1;
123
124 while (*challenge != '\0')
125 switch (digest_md5_getsubopt (&challenge, digest_challenge_opts, &value))
126 {
127 case CHALLENGE_REALM:
128 {
129 char **tmp;
130 out->nrealms++;
131 tmp = realloc (out->realms, out->nrealms * sizeof (*out->realms));
132 if (!tmp)
133 return -1;
134 out->realms = tmp;
135 out->realms[out->nrealms - 1] = strdup (value);
136 if (!out->realms[out->nrealms - 1])
137 return -1;
138 }
139 break;
140
141 case CHALLENGE_NONCE:
142 /* This directive is required and MUST appear exactly once; if
143 not present, or if multiple instances are present, the
144 client should abort the authentication exchange. */
145 if (out->nonce)
146 return -1;
147 out->nonce = strdup (value);
148 if (!out->nonce)
149 return -1;
150 break;
151
152 case CHALLENGE_QOP:
153 /* <<What if this directive is present multiple times? Error,
154 or take the union of all values?>> */
155 if (out->qops)
156 return -1;
157 {
158 char *subsubopts;
159 char *val;
160
161 subsubopts = value;
162 while (*subsubopts != '\0')
163 switch (digest_md5_getsubopt (&subsubopts, qop_opts, &val))
164 {
165 case QOP_AUTH:
166 out->qops |= DIGEST_MD5_QOP_AUTH;
167 break;
168
169 case QOP_AUTH_INT:
170 out->qops |= DIGEST_MD5_QOP_AUTH_INT;
171 break;
172
173 case QOP_AUTH_CONF:
174 out->qops |= DIGEST_MD5_QOP_AUTH_CONF;
175 break;
176
177 default:
178 /* The client MUST ignore unrecognized options */
179 break;
180 }
181 }
182 /* if the client recognizes no cipher, it MUST behave as if
183 "auth-conf" qop option wasn't provided by the server. */
184 if (disable_qop_auth_conf)
185 out->qops &= ~DIGEST_MD5_QOP_AUTH_CONF;
186 /* if the client recognizes no option, it MUST abort the
187 authentication exchange. */
188 if (!out->qops)
189 return -1;
190 break;
191
192 case CHALLENGE_STALE:
193 /* This directive may appear at most once; if multiple
194 instances are present, the client MUST abort the
195 authentication exchange. */
196 if (out->stale)
197 return -1;
198 out->stale = 1;
199 break;
200
201 case CHALLENGE_MAXBUF:
202 /* This directive may appear at most once; if multiple
203 instances are present, or the value is out of range the
204 client MUST abort the authentication exchange. */
205 if (out->servermaxbuf)
206 return -1;
207 out->servermaxbuf = strtoul (value, NULL, 10);
208 /* FIXME: error handling. */
209 /* The value MUST be bigger than 16 (32 for Confidentiality
210 protection with the "aes-cbc" cipher) and smaller or equal
211 to 16777215 (i.e. 2**24-1). */
212 if (out->servermaxbuf <= 16 || out->servermaxbuf > 16777215)
213 return -1;
214 break;
215
216 case CHALLENGE_CHARSET:
217 /* This directive may appear at most once; if multiple
218 instances are present, the client MUST abort the
219 authentication exchange. */
220 if (out->utf8)
221 return -1;
222 if (strcmp (DEFAULT_CHARSET, value) != 0)
223 return -1;
224 out->utf8 = 1;
225 break;
226
227 case CHALLENGE_ALGORITHM:
228 /* This directive is required and MUST appear exactly once; if
229 not present, or if multiple instances are present, the
230 client SHOULD abort the authentication exchange. */
231 if (done_algorithm)
232 return -1;
233 if (strcmp (DEFAULT_ALGORITHM, value) != 0)
234 return -1;
235 done_algorithm = 1;
236 break;
237
238
239 case CHALLENGE_CIPHER:
240 /* This directive must be present exactly once if "auth-conf"
241 is offered in the "qop-options" directive */
242 if (out->ciphers)
243 return -1;
244 {
245 char *subsubopts;
246 char *val;
247
248 subsubopts = value;
249 while (*subsubopts != '\0')
250 switch (digest_md5_getsubopt (&subsubopts, cipher_opts, &val))
251 {
252 case CIPHER_DES:
253 out->ciphers |= DIGEST_MD5_CIPHER_DES;
254 break;
255
256 case CIPHER_3DES:
257 out->ciphers |= DIGEST_MD5_CIPHER_3DES;
258 break;
259
260 case CIPHER_RC4:
261 out->ciphers |= DIGEST_MD5_CIPHER_RC4;
262 break;
263
264 case CIPHER_RC4_40:
265 out->ciphers |= DIGEST_MD5_CIPHER_RC4_40;
266 break;
267
268 case CIPHER_RC4_56:
269 out->ciphers |= DIGEST_MD5_CIPHER_RC4_56;
270 break;
271
272 case CIPHER_AES_CBC:
273 out->ciphers |= DIGEST_MD5_CIPHER_AES_CBC;
274 break;
275
276 default:
277 /* The client MUST ignore unrecognized ciphers */
278 break;
279 }
280 }
281 /* if the client recognizes no cipher, it MUST behave as if
282 "auth-conf" qop option wasn't provided by the server. */
283 if (!out->ciphers)
284 {
285 disable_qop_auth_conf = 1;
286 if (out->qops)
287 {
288 /* if the client recognizes no option, it MUST abort the
289 authentication exchange. */
290 out->qops &= ~DIGEST_MD5_QOP_AUTH_CONF;
291 if (!out->qops)
292 return -1;
293 }
294 }
295 break;
296
297 default:
298 /* The client MUST ignore any unrecognized directives. */
299 break;
300 }
301
302 /* This directive is required and MUST appear exactly once; if
303 not present, or if multiple instances are present, the
304 client SHOULD abort the authentication exchange. */
305 if (!done_algorithm)
306 return -1;
307
308 /* Validate that we have the mandatory fields. */
309 if (digest_md5_validate_challenge (out) != 0)
310 return -1;
311
312 return 0;
313}
314
315enum
316{
317 /* the order must match the following struct */
318 RESPONSE_USERNAME = 0,
319 RESPONSE_REALM,
320 RESPONSE_NONCE,
321 RESPONSE_CNONCE,
322 RESPONSE_NC,
323 RESPONSE_QOP,
324 RESPONSE_DIGEST_URI,
325 RESPONSE_RESPONSE,
326 RESPONSE_MAXBUF,
327 RESPONSE_CHARSET,
328 RESPONSE_CIPHER,
329 RESPONSE_AUTHZID
330};
331
332static const char *const digest_response_opts[] = {
333 /* the order must match the previous enum */
334 "username",
335 "realm",
336 "nonce",
337 "cnonce",
338 "nc",
339 "qop",
340 "digest-uri",
341 "response",
342 "maxbuf",
343 "charset",
344 "cipher",
345 "authzid",
346 NULL
347};
348
349static int
350parse_response (char *response, digest_md5_response * out)
351{
352 char *value;
353
354 memset (out, 0, sizeof (*out));
355
356 /* The size of a digest-response MUST be less than 4096 bytes. */
357 if (strlen (response) >= 4096)
358 return -1;
359
360 while (*response != '\0')
361 switch (digest_md5_getsubopt (&response, digest_response_opts, &value))
362 {
363 case RESPONSE_USERNAME:
364 /* This directive is required and MUST be present exactly
365 once; otherwise, authentication fails. */
366 if (out->username)
367 return -1;
368 out->username = strdup (value);
369 if (!out->username)
370 return -1;
371 break;
372
373 case RESPONSE_REALM:
374 /* This directive is required if the server provided any
375 realms in the "digest-challenge", in which case it may
376 appear exactly once and its value SHOULD be one of those
377 realms. */
378 if (out->realm)
379 return -1;
380 out->realm = strdup (value);
381 if (!out->realm)
382 return -1;
383 break;
384
385 case RESPONSE_NONCE:
386 /* This directive is required and MUST be present exactly
387 once; otherwise, authentication fails. */
388 if (out->nonce)
389 return -1;
390 out->nonce = strdup (value);
391 if (!out->nonce)
392 return -1;
393 break;
394
395 case RESPONSE_CNONCE:
396 /* This directive is required and MUST be present exactly once;
397 otherwise, authentication fails. */
398 if (out->cnonce)
399 return -1;
400 out->cnonce = strdup (value);
401 if (!out->cnonce)
402 return -1;
403 break;
404
405 case RESPONSE_NC:
406 /* This directive is required and MUST be present exactly
407 once; otherwise, authentication fails. */
408 if (out->nc)
409 return -1;
410 /* nc-value = 8LHEX */
411 if (strlen (value) != 8)
412 return -1;
413 out->nc = strtoul (value, NULL, 16);
414 /* FIXME: error handling. */
415 break;
416
417 case RESPONSE_QOP:
418 /* If present, it may appear exactly once and its value MUST
419 be one of the alternatives in qop-options. */
420 if (out->qop)
421 return -1;
422 if (strcmp (value, "auth") == 0)
423 out->qop = DIGEST_MD5_QOP_AUTH;
424 else if (strcmp (value, "auth-int") == 0)
425 out->qop = DIGEST_MD5_QOP_AUTH_INT;
426 else if (strcmp (value, "auth-conf") == 0)
427 out->qop = DIGEST_MD5_QOP_AUTH_CONF;
428 else
429 return -1;
430 break;
431
432 case RESPONSE_DIGEST_URI:
433 /* This directive is required and MUST be present exactly
434 once; if multiple instances are present, the client MUST
435 abort the authentication exchange. */
436 if (out->digesturi)
437 return -1;
438 /* FIXME: sub-parse. */
439 out->digesturi = strdup (value);
440 if (!out->digesturi)
441 return -1;
442 break;
443
444 case RESPONSE_RESPONSE:
445 /* This directive is required and MUST be present exactly
446 once; otherwise, authentication fails. */
447 if (*out->response)
448 return -1;
449 /* A string of 32 hex digits */
450 if (strlen (value) != DIGEST_MD5_RESPONSE_LENGTH)
451 return -1;
452 strcpy (out->response, value);
453 break;
454
455 case RESPONSE_MAXBUF:
456 /* This directive may appear at most once; if multiple
457 instances are present, the server MUST abort the
458 authentication exchange. */
459 if (out->clientmaxbuf)
460 return -1;
461 out->clientmaxbuf = strtoul (value, NULL, 10);
462 /* FIXME: error handling. */
463 /* If the value is less or equal to 16 (<<32 for aes-cbc>>) or
464 bigger than 16777215 (i.e. 2**24-1), the server MUST abort
465 the authentication exchange. */
466 if (out->clientmaxbuf <= 16 || out->clientmaxbuf > 16777215)
467 return -1;
468 break;
469
470 case RESPONSE_CHARSET:
471 if (strcmp (DEFAULT_CHARSET, value) != 0)
472 return -1;
473 out->utf8 = 1;
474 break;
475
476 case RESPONSE_CIPHER:
477 if (out->cipher)
478 return -1;
479 if (strcmp (value, "3des") == 0)
480 out->cipher = DIGEST_MD5_CIPHER_3DES;
481 else if (strcmp (value, "des") == 0)
482 out->cipher = DIGEST_MD5_CIPHER_DES;
483 else if (strcmp (value, "rc4-40") == 0)
484 out->cipher = DIGEST_MD5_CIPHER_RC4_40;
485 else if (strcmp (value, "rc4") == 0)
486 out->cipher = DIGEST_MD5_CIPHER_RC4;
487 else if (strcmp (value, "rc4-56") == 0)
488 out->cipher = DIGEST_MD5_CIPHER_RC4_56;
489 else if (strcmp (value, "aes-cbc") == 0)
490 out->cipher = DIGEST_MD5_CIPHER_AES_CBC;
491 else
492 return -1;
493 break;
494
495 case RESPONSE_AUTHZID:
496 /* This directive may appear at most once; if multiple
497 instances are present, the server MUST abort the
498 authentication exchange. <<FIXME NOT IN DRAFT>> */
499 if (out->authzid)
500 return -1;
501 /* The authzid MUST NOT be an empty string. */
502 if (*value == '\0')
503 return -1;
504 out->authzid = strdup (value);
505 if (!out->authzid)
506 return -1;
507 break;
508
509 default:
510 /* The client MUST ignore any unrecognized directives. */
511 break;
512 }
513
514 /* Validate that we have the mandatory fields. */
515 if (digest_md5_validate_response (out) != 0)
516 return -1;
517
518 return 0;
519}
520
521enum
522{
523 /* the order must match the following struct */
524 RESPONSEAUTH_RSPAUTH = 0
525};
526
527static const char *const digest_responseauth_opts[] = {
528 /* the order must match the previous enum */
529 "rspauth",
530 NULL
531};
532
533static int
534parse_finish (char *finish, digest_md5_finish * out)
535{
536 char *value;
537
538 memset (out, 0, sizeof (*out));
539
540 /* The size of a response-auth MUST be less than 2048 bytes. */
541 if (strlen (finish) >= 2048)
542 return -1;
543
544 while (*finish != '\0')
545 switch (digest_md5_getsubopt (&finish, digest_responseauth_opts, &value))
546 {
547 case RESPONSEAUTH_RSPAUTH:
548 if (*out->rspauth)
549 return -1;
550 /* A string of 32 hex digits */
551 if (strlen (value) != DIGEST_MD5_RESPONSE_LENGTH)
552 return -1;
553 strcpy (out->rspauth, value);
554 break;
555
556 default:
557 /* The client MUST ignore any unrecognized directives. */
558 break;
559 }
560
561 /* Validate that we have the mandatory fields. */
562 if (digest_md5_validate_finish (out) != 0)
563 return -1;
564
565 return 0;
566}
567
568int
569digest_md5_parse_challenge (const char *challenge, size_t len,
570 digest_md5_challenge * out)
571{
572 char *subopts = len ? strndup (challenge, len) : strdup (challenge);
573 int rc;
574
575 if (!subopts)
576 return -1;
577
578 rc = parse_challenge (subopts, out);
579
580 free (subopts);
581
582 return rc;
583}
584
585int
586digest_md5_parse_response (const char *response, size_t len,
587 digest_md5_response * out)
588{
589 char *subopts = len ? strndup (response, len) : strdup (response);
590 int rc;
591
592 if (!subopts)
593 return -1;
594
595 rc = parse_response (subopts, out);
596
597 free (subopts);
598
599 return rc;
600}
601
602int
603digest_md5_parse_finish (const char *finish, size_t len,
604 digest_md5_finish * out)
605{
606 char *subopts = len ? strndup (finish, len) : strdup (finish);
607 int rc;
608
609 if (!subopts)
610 return -1;
611
612 rc = parse_finish (subopts, out);
613
614 free (subopts);
615
616 return rc;
617}
618