1 /* parser.c --- DIGEST-MD5 parser.
2  * Copyright (C) 2002-2021 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
parse_challenge(char * challenge,digest_md5_challenge * out)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
parse_response(char * response,digest_md5_response * out)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
parse_finish(char * finish,digest_md5_finish * out)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
digest_md5_parse_challenge(const char * challenge,size_t len,digest_md5_challenge * out)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
digest_md5_parse_response(const char * response,size_t len,digest_md5_response * out)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
digest_md5_parse_finish(const char * finish,size_t len,digest_md5_finish * out)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