chiark / gitweb /
6239eb0c62dc023430d66c6be790a619b7130241
[catacomb] / progs / cookie.c
1 /* -*-c-*-
2  *
3  * Generate and validate cryptographic cookies
4  *
5  * (c) 1999 Mark Wooding
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of Catacomb.
11  *
12  * Catacomb is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * Catacomb is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with Catacomb; if not, write to the Free Software Foundation,
24  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25  */
26
27 /*----- Header files ------------------------------------------------------*/
28
29 #define _FILE_OFFSET_BITS 64
30
31 #include "config.h"
32
33 #include <errno.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <time.h>
38
39 #include <mLib/base64.h>
40 #include <mLib/bits.h>
41 #include <mLib/dstr.h>
42 #include <mLib/mdwopt.h>
43 #include <mLib/quis.h>
44 #include <mLib/report.h>
45 #include <mLib/sub.h>
46
47 #include "cc.h"
48 #include "ct.h"
49 #include "key.h"
50 #include "gmac.h"
51 #include "getdate.h"
52
53 /*----- Handy global state ------------------------------------------------*/
54
55 static const char *keyfile = "keyring";
56
57 /*----- Cookie format -----------------------------------------------------*/
58
59 /* --- Cookie header structure (unpacked) --- */
60
61 typedef struct cookie {
62   uint32 k;
63   time_t exp;
64 } cookie;
65
66 /* --- Size of a cookie header (packed) --- */
67
68 #define COOKIE_SZ (4 + 8)
69
70 /* --- @COOKIE_PACK@ --- *
71  *
72  * Arguments:   @p@ = pointer to destination buffer
73  *              @c@ = pointer to source cookie header block
74  *
75  * Use:         Packs a cookie header into an octet buffer in a machine-
76  *              independent way.
77  */
78
79 #define COOKIE_PACK(p, c) do {                                          \
80   octet *_p = (octet *)(p);                                             \
81   const cookie *_c = (c);                                               \
82   STORE32(_p + 0, _c->k);                                               \
83   STORE32(_p + 4, ((_c->exp & ~MASK32) >> 16) >> 16);                   \
84   STORE32(_p + 8, _c->exp);                                             \
85 } while (0)
86
87 /* --- @COOKIE_UNPACK@ --- *
88  *
89  * Arguments:   @c@ = pointer to destination cookie header
90  *              @p@ = pointer to source buffer
91  *
92  * Use:         Unpacks a cookie header from an octet buffer into a
93  *              machine-specific but comprehensible structure.
94  */
95
96 #define COOKIE_UNPACK(c, p) do {                                        \
97   cookie *_c = (c);                                                     \
98   const octet *_p = (const octet *)(p);                                 \
99   _c->k = LOAD32(_p + 0);                                               \
100   _c->exp = ((time_t)(((LOAD32(_p + 4) << 16) << 16) & ~MASK32) |       \
101              (time_t)LOAD32(_p + 8));                                   \
102 } while (0)
103
104 /*----- Useful shared functions -------------------------------------------*/
105
106 /* --- @doopen@ --- *
107  *
108  * Arguments:   @key_file *f@ = pointer to key file block
109  *              @unsigned how@ = method to open file with
110  *
111  * Returns:     ---
112  *
113  * Use:         Opens a key file and handles errors by panicking
114  *              appropriately.
115  */
116
117 static void doopen(key_file *f, unsigned how)
118 {
119   if (key_open(f, keyfile, how, key_moan, 0)) {
120     die(EXIT_FAILURE, "couldn't open file `%s': %s",
121         keyfile, strerror(errno));
122   }
123 }
124
125 /* --- @doclose@ --- *
126  *
127  * Arguments:   @key_file *f@ = pointer to key file block
128  *
129  * Returns:     ---
130  *
131  * Use:         Closes a key file and handles errors by panicking
132  *              appropriately.
133  */
134
135 static void doclose(key_file *f)
136 {
137   switch (key_close(f)) {
138     case KWRITE_FAIL:
139       die(EXIT_FAILURE, "couldn't write file `%s': %s",
140           keyfile, strerror(errno));
141     case KWRITE_BROKEN:
142       die(EXIT_FAILURE, "keyring file `%s' broken: %s (repair manually)",
143           keyfile, strerror(errno));
144   }
145 }
146
147 /* --- @getmac@ --- *
148  *
149  * Arguments:   @key *k@ = key to use
150  *              @const char *app@ = application name
151  *
152  * Returns:     The MAC to use.
153  *
154  * Use:         Finds the right MAC for the given key.
155  */
156
157 static gmac *getmac(key *k, const char *app)
158 {
159   dstr t = DSTR_INIT;
160   dstr d = DSTR_INIT;
161   char *p = 0;
162   const char *q;
163   size_t n;
164   key_bin kb;
165   key_packdef kp;
166   const gcmac *cm;
167   int e;
168   gmac *m;
169
170   /* --- Set up --- */
171
172   key_fulltag(k, &t);
173
174   /* --- Pick out the right MAC --- */
175
176   n = strlen(app);
177   if ((q = key_getattr(0, k, "mac")) != 0) {
178     dstr_puts(&d, q);
179     p = d.buf;
180   } else if (strncmp(k->type, app, n) == 0 && k->type[n] == '-') {
181     dstr_puts(&d, k->type);
182     p = d.buf + n + 1;
183   } else
184     die(EXIT_FAILURE, "no MAC algorithm for key `%s'", t.buf);
185   if ((cm = gmac_byname(p)) == 0) {
186     die(EXIT_FAILURE, "MAC algorithm `%s' not found in key `%s'",
187         p, t.buf);
188   }
189
190   /* --- Unlock the key --- */
191
192   kp.e = KENC_BINARY;
193   kp.p = &kb;
194   if ((e = key_unpack(&kp, k->k, &t)) != 0) {
195     die(EXIT_FAILURE, "error unpacking key `%s': %s",
196         t.buf, key_strerror(e));
197   }
198
199   /* --- Make the MAC object --- */
200
201   if (keysz(kb.sz, cm->keysz) != kb.sz)
202     die(EXIT_FAILURE, "key %s has bad length (%lu) for MAC %s",
203         t.buf, (unsigned long)kb.sz, cm->name);
204   m = cm->key(kb.k, kb.sz);
205   key_unpackdone(&kp);
206   return (m);
207 }
208
209 /*----- Command implementation --------------------------------------------*/
210
211 /* --- @cmd_gen@ --- */
212
213 static int cmd_gen(int argc, char *argv[])
214 {
215   key_file f;
216   key *k;
217   gmac *m;
218   ghash *h;
219   const char *tag = "cookie";
220   int err;
221   cookie c = { 0, KEXP_EXPIRE };
222   unsigned fl = 0;
223   int bits = 32;
224   const octet *t;
225   dstr d = DSTR_INIT;
226   octet buf[COOKIE_SZ];
227   base64_ctx b;
228
229   /* --- Various useful flag bits --- */
230
231 #define f_bogus 1u
232
233   /* --- Parse options for the subcommand --- */
234
235   for (;;) {
236     static struct option opt[] = {
237       { "bits",         OPTF_ARGREQ,    0,      'b' },
238       { "expire",       OPTF_ARGREQ,    0,      'e' },
239       { "key",          OPTF_ARGREQ,    0,      'k' },
240       { 0,              0,              0,      0 }
241     };
242     int i = mdwopt(argc, argv, "+b:e:i:t:", opt, 0, 0, 0);
243     if (i < 0)
244       break;
245
246     /* --- Handle the various options --- */
247
248     switch (i) {
249
250       /* --- Fetch a size in bits --- */
251
252       case 'b':
253         if (!(bits = atoi(optarg)) || bits % 8)
254           die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
255         break;
256
257       /* --- Fetch an expiry time --- */
258
259       case 'e':
260         if (strcmp(optarg, "forever") == 0)
261           c.exp = KEXP_FOREVER;
262         else if ((c.exp = get_date(optarg, 0)) == -1)
263           die(EXIT_FAILURE, "bad expiry date: `%s'", optarg);
264         break;
265
266       /* --- Fetch a key type --- */
267
268       case 'k':
269         tag = optarg;
270         break;
271
272       /* --- Other things are bogus --- */
273
274       default:
275         fl |= f_bogus;
276         break;
277     }
278   }
279
280   /* --- Various sorts of bogosity --- */
281
282   if (fl & f_bogus || optind + 1 < argc)
283     die(EXIT_FAILURE,
284         "Usage: generate [-b BITS] [-e TIME] [-k TAG] [DATA]");
285
286   /* --- Choose a default expiry time --- */
287
288   if (c.exp == KEXP_EXPIRE)
289     c.exp = time(0) + 7 * 24 * 60 * 60;
290
291   /* --- Open the key file and get the key --- */
292
293   doopen(&f, KOPEN_WRITE);
294   if ((k = key_bytag(&f, tag)) == 0) {
295     die(EXIT_FAILURE, "no key with tag `%s' in keyring `%s'",
296         tag, keyfile);
297   }
298
299   c.k = k->id;
300   if ((err = key_used(&f, k, c.exp)) != 0)
301     die(EXIT_FAILURE, "can't generate cookie: %s", key_strerror(err));
302   m = getmac(k, "cookie");
303   if (bits/8 > GM_CLASS(m)->hashsz) {
304     die(EXIT_FAILURE, "inapproriate bit length for `%s' MACs",
305         GM_CLASS(m)->name);
306   }
307
308   /* --- Store and MAC the cookie --- */
309
310   COOKIE_PACK(buf, &c);
311
312   h = GM_INIT(m);
313   GH_HASH(h, buf, sizeof(buf));
314   if (argv[optind])
315     GH_HASH(h, argv[optind], strlen(argv[optind]));
316   t = GH_DONE(h, 0);
317
318   /* --- Encode and emit the finished cookie --- */
319
320   base64_init(&b);
321   b.indent = "";
322   base64_encode(&b, buf, sizeof(buf), &d);
323   base64_encode(&b, t, bits/8, &d);
324   base64_encode(&b, 0, 0, &d);
325   DWRITE(&d, stdout);
326   fputc('\n', stdout);
327   DDESTROY(&d);
328   GH_DESTROY(h);
329   GM_DESTROY(m);
330
331   doclose(&f);
332   return (0);
333
334 #undef f_bogus
335 }
336
337 /* --- @cmd_verify@ --- */
338
339 static int cmd_verify(int argc, char *argv[])
340 {
341   key_file f;
342   dstr d = DSTR_INIT;
343   unsigned fl = 0;
344   int bits = -1, minbits = 32;
345   int v = 1;
346   base64_ctx b;
347   gmac *m;
348   ghash *h;
349   cookie c;
350   key *k;
351   int cbits;
352   const octet *t;
353   time_t now = time(0);
354
355   /* --- Various useful flag bits --- */
356
357 #define f_bogus 1u
358 #define f_forever 2u
359 #define f_utc 4u
360
361   /* --- Parse options for the subcommand --- */
362
363   for (;;) {
364     static struct option opt[] = {
365       { "bits",         OPTF_ARGREQ,    0,      'b' },
366       { "min-bits",     OPTF_ARGREQ,    0,      'm' },
367       { "forever",      0,              0,      'f' },
368       { "quiet",        0,              0,      'q' },
369       { "verbose",      0,              0,      'v' },
370       { "utc",          0,              0,      'u' },
371       { 0,              0,              0,      0 }
372     };
373     int i = mdwopt(argc, argv, "+b:m:fqvu", opt, 0, 0, 0);
374     if (i < 0)
375       break;
376
377     /* --- Handle the various options --- */
378
379     switch (i) {
380
381       /* --- Fetch a size in bits --- */
382
383       case 'b':
384         if (!(bits = atoi(optarg)) || bits % 8)
385           die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
386         break;
387       case 'm':
388         if (!(minbits = atoi(optarg)) || minbits % 8)
389           die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
390         break;
391
392       /* --- Miscellaneous flags --- */
393
394       case 'f':
395         fl |= f_forever;
396         break;
397       case 'u':
398         fl |= f_utc;
399         break;
400       case 'q':
401         if (v > 0) v--;
402         break;
403       case 'v':
404         v++;
405         break;
406
407       /* --- Other things are bogus --- */
408
409       default:
410         fl |= f_bogus;
411         break;
412     }
413   }
414
415   /* --- Various sorts of bogosity --- */
416
417   if (fl & f_bogus || optind == argc || optind + 2 < argc) {
418     die(EXIT_FAILURE,
419         "Usage: verify [-fuqv] [-b BITS] [-m BITS] COOKIE [DATA]");
420   }
421   doopen(&f, KOPEN_READ);
422
423   /* --- Decode the base64 wrapping --- */
424
425   base64_init(&b);
426   base64_decode(&b, argv[optind], strlen(argv[optind]), &d);
427   base64_decode(&b, 0, 0, &d);
428
429   if (d.len < COOKIE_SZ + 1) {
430     if (v) printf("FAIL cookie too small\n");
431     goto fail;
432   }
433
434   /* --- Extract the relevant details --- */
435
436   COOKIE_UNPACK(&c, d.buf);
437
438   if (v > 1) {
439     char buf[64];
440     if (c.exp == KEXP_FOREVER)
441       strcpy(buf, "forever");
442     else {
443       struct tm *tm;
444       const char *fmt;
445
446       if (fl & f_utc) {
447         tm = gmtime(&c.exp);
448         fmt = "%Y-%m-%d %H:%M:%S UTC";
449       } else {
450         tm = localtime(&c.exp);
451         fmt = "%Y-%m-%d %H:%M:%S %Z";
452       }
453       strftime(buf, sizeof(buf), fmt, tm);
454     }
455     printf("INFO keyid = %08lx; expiry = %s\n", (unsigned long)c.k, buf);
456   }
457
458   /* --- Check the authentication token width --- */
459
460   cbits = (d.len - COOKIE_SZ) * 8;
461   if (v > 2) printf("INFO authentication token width = %i bits\n", cbits);
462   if (bits == -1) {
463     if (cbits < minbits) {
464       if (v) printf("FAIL authentication token too narrow\n");
465       goto fail;
466     }
467   } else {
468     if (cbits != bits) {
469       if (v) printf("FAIL authentication token width doesn't match\n");
470       goto fail;
471     }
472   }
473   /* --- Get the key --- */
474
475   if ((k = key_byid(&f, c.k)) == 0) {
476     if (v) printf("FAIL keyid %08lx unavailable\n", (unsigned long)c.k);
477     goto fail;
478   }
479
480   /* --- Check that the cookie authenticates OK --- */
481
482   m = getmac(k, "cookie");
483   h = GM_INIT(m);
484   GH_HASH(h, d.buf, COOKIE_SZ);
485   if (argv[optind + 1])
486     GH_HASH(h, argv[optind + 1], strlen(argv[optind + 1]));
487   t = GH_DONE(h, 0);
488
489   if (!ct_memeq(t, d.buf + COOKIE_SZ, cbits / 8)) {
490     if (v) printf("FAIL bad authentication token\n");
491     goto fail;
492   }
493
494   /* --- See whether the cookie has expired --- */
495
496   if (c.exp == KEXP_FOREVER) {
497     if (!(fl & f_forever)) {
498       if (v) printf("FAIL forever cookies not allowed\n");
499       goto fail;
500     }
501     if (k->exp != KEXP_FOREVER) {
502       if (v) printf("FAIL cookie lasts forever but key will expire\n");
503       goto fail;
504     }
505   } else if (c.exp < now) {
506     if (v) printf("FAIL cookie has expired\n");
507     goto fail;
508   }
509
510   if (v) printf("OK\n");
511   key_close(&f);
512   GM_DESTROY(m);
513   GH_DESTROY(h);
514   dstr_destroy(&d);
515   return (0);
516
517 fail:
518   key_close(&f);
519   dstr_destroy(&d);
520   return (1);
521
522 #undef f_bogus
523 #undef f_forever
524 #undef f_utc
525 }
526
527 /*----- Main command table ------------------------------------------------*/
528
529 static int cmd_help(int, char **);
530
531 #define LISTS(LI)                                                       \
532   LI("Lists", list,                                                     \
533      listtab[i].name, listtab[i].name)                                  \
534   LI("Message authentication algorithms", mac,                          \
535      gmactab[i], gmactab[i]->name)
536
537 MAKELISTTAB(listtab, LISTS)
538
539 static int cmd_show(int argc, char *argv[])
540 {
541   return (displaylists(listtab, argv + 1));
542 }
543
544 static cmd cmds[] = {
545   { "help", cmd_help, "help [COMMAND...]" },
546   { "show", cmd_show, "show [ITEM...]" },
547   { "generate", cmd_gen,
548     "generate [-b BITS] [-e TIME] [-k TAG] [DATA]", "\
549 Options:\n\
550 \n\
551 -b, --bits=N            Use an N-bit token in the cookie.\n\
552 -e, --expire=TIME       Make the cookie expire after TIME.\n\
553 -k, --key=TAG           Use key TAG to create the token.\n\
554 " },
555   { "verify", cmd_verify,
556     "verify [-fuqv] [-b BITS] [-m BITS] COOKIE [DATA]", "\
557 Options:\n\
558 \n\
559 -b, --bits=N            Accept tokens exactly N bits long only.\n\
560 -m, --min-bits=N        Accept tokens N bits long or more.\n\
561 -f, --forever           Accept cookies which never expire.\n\
562 -u, --utc               Output cookie expiry dates in UTC.\n\
563 -q, --quiet             Produce less output while checking cookies.\n\
564 -v, --verbose           Produce more output while checking cookies.\n\
565 " },
566   { 0, 0, 0 }
567 };
568
569 static int cmd_help(int argc, char *argv[])
570 {
571   sc_help(cmds, stdout, argv + 1);
572   return (0);
573 }
574
575 /*----- Main code ---------------------------------------------------------*/
576
577 /* --- Helpful GNUy functions --- */
578
579 static void usage(FILE *fp)
580 {
581   fprintf(fp, "Usage: %s [-k KEYRING] COMMAND [ARGS]\n", QUIS);
582 }
583
584 void version(FILE *fp)
585 {
586   fprintf(fp, "%s, Catacomb version " VERSION "\n", QUIS);
587 }
588
589 void help_global(FILE *fp)
590 {
591   usage(fp);
592   fputs("\n\
593 Generates and validates cryptographic cookies.  Command line options\n\
594 recognized are:\n\
595 \n\
596 -h, --help [COMMAND]    Display this help text (or help for COMMAND).\n\
597 -v, --version           Display version number.\n\
598 -u, --usage             Display short usage summary.\n\
599 \n\
600 -k, --key-file=FILE     Read and write keys in FILE.\n",
601         fp);
602 }
603
604 /* --- @main@ --- *
605  *
606  * Arguments:   @int argc@ = number of command line arguments
607  *              @char *argv[]@ = array of arguments
608  *
609  * Returns:     Zero if OK, nonzero if not.
610  *
611  * Use:         Generates and validates cryptographic cookies.
612  */
613
614 int main(int argc, char *argv[])
615 {
616   unsigned f = 0;
617
618 #define f_bogus 1u
619 #define f_forever 2u
620
621   /* --- Initialize the library --- */
622
623   ego(argv[0]);
624   sub_init();
625
626   /* --- Options parsing --- */
627
628   for (;;) {
629     static struct option opt[] = {
630
631       /* --- Standard GNUy help options --- */
632
633       { "help",         0,              0,      'h' },
634       { "version",      0,              0,      'v' },
635       { "usage",        0,              0,      'u' },
636
637       /* --- Actual relevant options --- */
638
639       { "keyring",      OPTF_ARGREQ,    0,      'k' },
640
641       /* --- Magic terminator --- */
642
643       { 0,              0,              0,      0 }
644     };
645     int i = mdwopt(argc, argv, "+hvu k:", opt, 0, 0, 0);
646
647     if (i < 0)
648       break;
649     switch (i) {
650
651       /* --- Helpful GNUs --- */
652
653       case 'u':
654         usage(stdout);
655         exit(0);
656       case 'v':
657         version(stdout);
658         exit(0);
659       case 'h':
660         sc_help(cmds, stdout, argv + optind);
661         exit(0);
662
663       /* --- Real genuine useful options --- */
664
665       case 'k':
666         keyfile = optarg;
667         break;
668
669       /* --- Bogus things --- */
670
671       default:
672         f |= f_bogus;
673         break;
674     }
675   }
676
677   if ((f & f_bogus) || optind == argc) {
678     usage(stderr);
679     exit(EXIT_FAILURE);
680   }
681
682   /* --- Dispatch to appropriate command handler --- */
683
684   argc -= optind;
685   argv += optind;
686   optind = 0;
687   return (findcmd(cmds, argv[0])->cmd(argc, argv));
688
689 #undef f_bogus
690 #undef f_forever
691 }
692
693 /*----- That's all, folks -------------------------------------------------*/