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 | |
42 | enum |
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 | |
55 | static 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 */ |
69 | enum |
70 | { |
71 | /* the order must match the following struct */ |
72 | QOP_AUTH = 0, |
73 | QOP_AUTH_INT, |
74 | QOP_AUTH_CONF |
75 | }; |
76 | |
77 | static 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 | */ |
89 | enum |
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 | |
100 | static 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 | |
111 | static int |
112 | parse_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 | |
315 | enum |
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 | |
332 | static 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 | |
349 | static int |
350 | parse_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 | |
521 | enum |
522 | { |
523 | /* the order must match the following struct */ |
524 | RESPONSEAUTH_RSPAUTH = 0 |
525 | }; |
526 | |
527 | static const char *const digest_responseauth_opts[] = { |
528 | /* the order must match the previous enum */ |
529 | "rspauth" , |
530 | NULL |
531 | }; |
532 | |
533 | static int |
534 | parse_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 | |
568 | int |
569 | digest_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 | |
585 | int |
586 | digest_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 | |
602 | int |
603 | digest_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 | |