+/* -*-c-*-
+ *
+ * $Id$
+ *
+ * Generate and validate cryptographic cookies
+ *
+ * (c) 1999 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Catacomb.
+ *
+ * Catacomb is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Catacomb is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Catacomb; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <mLib/base64.h>
+#include <mLib/bits.h>
+#include <mLib/dstr.h>
+#include <mLib/mdwopt.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+#include <mLib/sub.h>
+
+#include "cc.h"
+#include "key.h"
+#include "gmac.h"
+#include "getdate.h"
+
+/*----- Handy global state ------------------------------------------------*/
+
+static const char *keyfile = "keyring";
+
+/*----- Cookie format -----------------------------------------------------*/
+
+/* --- Cookie header structure (unpacked) --- */
+
+typedef struct cookie {
+ uint32 k;
+ time_t exp;
+} cookie;
+
+/* --- Size of a cookie header (packed) --- */
+
+#define COOKIE_SZ (4 + 8)
+
+/* --- @COOKIE_PACK@ --- *
+ *
+ * Arguments: @p@ = pointer to destination buffer
+ * @c@ = pointer to source cookie header block
+ *
+ * Use: Packs a cookie header into an octet buffer in a machine-
+ * independent way.
+ */
+
+#define COOKIE_PACK(p, c) do { \
+ octet *_p = (octet *)(p); \
+ const cookie *_c = (c); \
+ STORE32(_p + 0, _c->k); \
+ STORE32(_p + 4, ((_c->exp & ~MASK32) >> 16) >> 16); \
+ STORE32(_p + 8, _c->exp); \
+} while (0)
+
+/* --- @COOKIE_UNPACK@ --- *
+ *
+ * Arguments: @c@ = pointer to destination cookie header
+ * @p@ = pointer to source buffer
+ *
+ * Use: Unpacks a cookie header from an octet buffer into a
+ * machine-specific but comprehensible structure.
+ */
+
+#define COOKIE_UNPACK(c, p) do { \
+ cookie *_c = (c); \
+ const octet *_p = (const octet *)(p); \
+ _c->k = LOAD32(_p + 0); \
+ _c->exp = ((time_t)(((LOAD32(_p + 4) << 16) << 16) & ~MASK32) | \
+ (time_t)LOAD32(_p + 8)); \
+} while (0)
+
+/*----- Useful shared functions -------------------------------------------*/
+
+/* --- @doopen@ --- *
+ *
+ * Arguments: @key_file *f@ = pointer to key file block
+ * @unsigned how@ = method to open file with
+ *
+ * Returns: ---
+ *
+ * Use: Opens a key file and handles errors by panicking
+ * appropriately.
+ */
+
+static void doopen(key_file *f, unsigned how)
+{
+ if (key_open(f, keyfile, how, key_moan, 0)) {
+ die(EXIT_FAILURE, "couldn't open file `%s': %s",
+ keyfile, strerror(errno));
+ }
+}
+
+/* --- @doclose@ --- *
+ *
+ * Arguments: @key_file *f@ = pointer to key file block
+ *
+ * Returns: ---
+ *
+ * Use: Closes a key file and handles errors by panicking
+ * appropriately.
+ */
+
+static void doclose(key_file *f)
+{
+ switch (key_close(f)) {
+ case KWRITE_FAIL:
+ die(EXIT_FAILURE, "couldn't write file `%s': %s",
+ keyfile, strerror(errno));
+ case KWRITE_BROKEN:
+ die(EXIT_FAILURE, "keyring file `%s' broken: %s (repair manually)",
+ keyfile, strerror(errno));
+ }
+}
+
+/* --- @getmac@ --- *
+ *
+ * Arguments: @key *k@ = key to use
+ * @const char *app@ = application name
+ *
+ * Returns: The MAC to use.
+ *
+ * Use: Finds the right MAC for the given key.
+ */
+
+static gmac *getmac(key *k, const char *app)
+{
+ dstr t = DSTR_INIT;
+ dstr d = DSTR_INIT;
+ char *p = 0;
+ const char *q;
+ size_t n;
+ key_bin kb;
+ key_packdef kp;
+ const gcmac *cm;
+ int e;
+ gmac *m;
+
+ /* --- Set up --- */
+
+ key_fulltag(k, &t);
+
+ /* --- Pick out the right MAC --- */
+
+ n = strlen(app);
+ if ((q = key_getattr(0, k, "mac")) != 0) {
+ dstr_puts(&d, q);
+ p = d.buf;
+ } else if (strncmp(k->type, app, n) == 0 && k->type[n] == '-') {
+ dstr_puts(&d, k->type);
+ p = d.buf + n + 1;
+ } else
+ die(EXIT_FAILURE, "no MAC algorithm for key `%s'", t.buf);
+ if ((cm = gmac_byname(p)) == 0) {
+ die(EXIT_FAILURE, "MAC algorithm `%s' not found in key `%s'",
+ p, t.buf);
+ }
+
+ /* --- Unlock the key --- */
+
+ kp.kd.e = KENC_BINARY;
+ kp.p = &kb;
+ if ((e = key_unpack(&kp, &k->k, &t)) != 0) {
+ die(EXIT_FAILURE, "error unpacking key `%s': %s",
+ t.buf, key_strerror(e));
+ }
+
+ /* --- Make the MAC object --- */
+
+ if (keysz(kb.sz, cm->keysz) != kb.sz)
+ die(EXIT_FAILURE, "key %s has bad length (%lu) for MAC %s",
+ t.buf, (unsigned long)kb.sz, cm->name);
+ m = cm->key(kb.k, kb.sz);
+ key_unpackdone(&kp);
+ return (m);
+}
+
+/*----- Command implementation --------------------------------------------*/
+
+/* --- @cmd_gen@ --- */
+
+static int cmd_gen(int argc, char *argv[])
+{
+ key_file f;
+ key *k;
+ gmac *m;
+ ghash *h;
+ const char *tag = "cookie";
+ int err;
+ cookie c = { 0, KEXP_EXPIRE };
+ unsigned fl = 0;
+ int bits = 32;
+ const octet *t;
+ dstr d = DSTR_INIT;
+ octet buf[COOKIE_SZ];
+ base64_ctx b;
+
+ /* --- Various useful flag bits --- */
+
+#define f_bogus 1u
+
+ /* --- Parse options for the subcommand --- */
+
+ for (;;) {
+ static struct option opt[] = {
+ { "bits", OPTF_ARGREQ, 0, 'b' },
+ { "expire", OPTF_ARGREQ, 0, 'e' },
+ { "key", OPTF_ARGREQ, 0, 'k' },
+ { 0, 0, 0, 0 }
+ };
+ int i = mdwopt(argc, argv, "+b:e:i:t:", opt, 0, 0, 0);
+ if (i < 0)
+ break;
+
+ /* --- Handle the various options --- */
+
+ switch (i) {
+
+ /* --- Fetch a size in bits --- */
+
+ case 'b':
+ if (!(bits = atoi(optarg)) || bits % 8)
+ die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
+ break;
+
+ /* --- Fetch an expiry time --- */
+
+ case 'e':
+ if (strcmp(optarg, "forever") == 0)
+ c.exp = KEXP_FOREVER;
+ else if ((c.exp = get_date(optarg, 0)) == -1)
+ die(EXIT_FAILURE, "bad expiry date: `%s'", optarg);
+ break;
+
+ /* --- Fetch a key type --- */
+
+ case 'k':
+ tag = optarg;
+ break;
+
+ /* --- Other things are bogus --- */
+
+ default:
+ fl |= f_bogus;
+ break;
+ }
+ }
+
+ /* --- Various sorts of bogosity --- */
+
+ if (fl & f_bogus || optind + 1 < argc)
+ die(EXIT_FAILURE,
+ "Usage: generate [-b BITS] [-e TIME] [-k TAG] [DATA]");
+
+ /* --- Choose a default expiry time --- */
+
+ if (c.exp == KEXP_EXPIRE)
+ c.exp = time(0) + 7 * 24 * 60 * 60;
+
+ /* --- Open the key file and get the key --- */
+
+ doopen(&f, KOPEN_WRITE);
+ if ((k = key_bytag(&f, tag)) == 0) {
+ die(EXIT_FAILURE, "no key with tag `%s' in keyring `%s'",
+ tag, keyfile);
+ }
+
+ c.k = k->id;
+ if ((err = key_used(&f, k, c.exp)) != 0)
+ die(EXIT_FAILURE, "can't generate cookie: %s", key_strerror(err));
+ m = getmac(k, "cookie");
+ if (bits/8 > GM_CLASS(m)->hashsz) {
+ die(EXIT_FAILURE, "inapproriate bit length for `%s' MACs",
+ GM_CLASS(m)->name);
+ }
+
+ /* --- Store and MAC the cookie --- */
+
+ COOKIE_PACK(buf, &c);
+
+ h = GM_INIT(m);
+ GH_HASH(h, buf, sizeof(buf));
+ if (argv[optind])
+ GH_HASH(h, argv[optind], strlen(argv[optind]));
+ t = GH_DONE(h, 0);
+
+ /* --- Encode and emit the finished cookie --- */
+
+ base64_init(&b);
+ b.indent = "";
+ base64_encode(&b, buf, sizeof(buf), &d);
+ base64_encode(&b, t, bits/8, &d);
+ base64_encode(&b, 0, 0, &d);
+ DWRITE(&d, stdout);
+ fputc('\n', stdout);
+ DDESTROY(&d);
+ GH_DESTROY(h);
+ GM_DESTROY(m);
+
+ doclose(&f);
+ return (0);
+
+#undef f_bogus
+}
+
+/* --- @cmd_verify@ --- */
+
+static int cmd_verify(int argc, char *argv[])
+{
+ key_file f;
+ dstr d = DSTR_INIT;
+ unsigned fl = 0;
+ int bits = -1, minbits = 32;
+ int v = 1;
+ base64_ctx b;
+ gmac *m;
+ ghash *h;
+ cookie c;
+ key *k;
+ int cbits;
+ const octet *t;
+ time_t now = time(0);
+
+ /* --- Various useful flag bits --- */
+
+#define f_bogus 1u
+#define f_forever 2u
+#define f_utc 4u
+
+ /* --- Parse options for the subcommand --- */
+
+ for (;;) {
+ static struct option opt[] = {
+ { "bits", OPTF_ARGREQ, 0, 'b' },
+ { "min-bits", OPTF_ARGREQ, 0, 'm' },
+ { "forever", 0, 0, 'f' },
+ { "quiet", 0, 0, 'q' },
+ { "verbose", 0, 0, 'v' },
+ { "utc", 0, 0, 'u' },
+ { 0, 0, 0, 0 }
+ };
+ int i = mdwopt(argc, argv, "+b:m:fqvu", opt, 0, 0, 0);
+ if (i < 0)
+ break;
+
+ /* --- Handle the various options --- */
+
+ switch (i) {
+
+ /* --- Fetch a size in bits --- */
+
+ case 'b':
+ if (!(bits = atoi(optarg)) || bits % 8)
+ die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
+ break;
+ case 'm':
+ if (!(minbits = atoi(optarg)) || minbits % 8)
+ die(EXIT_FAILURE, "bad number of bits: `%s'", optarg);
+ break;
+
+ /* --- Miscellaneous flags --- */
+
+ case 'f':
+ fl |= f_forever;
+ break;
+ case 'u':
+ fl |= f_utc;
+ break;
+ case 'q':
+ if (v > 0) v--;
+ break;
+ case 'v':
+ v++;
+ break;
+
+ /* --- Other things are bogus --- */
+
+ default:
+ fl |= f_bogus;
+ break;
+ }
+ }
+
+ /* --- Various sorts of bogosity --- */
+
+ if (fl & f_bogus || optind == argc || optind + 2 < argc) {
+ die(EXIT_FAILURE,
+ "Usage: verify [-fuqv] [-b BITS] [-m BITS] COOKIE [DATA]");
+ }
+ doopen(&f, KOPEN_READ);
+
+ /* --- Decode the base64 wrapping --- */
+
+ base64_init(&b);
+ base64_decode(&b, argv[optind], strlen(argv[optind]), &d);
+ base64_decode(&b, 0, 0, &d);
+
+ if (d.len < COOKIE_SZ + 1) {
+ if (v) printf("FAIL cookie too small\n");
+ goto fail;
+ }
+
+ /* --- Extract the relevant details --- */
+
+ COOKIE_UNPACK(&c, d.buf);
+
+ if (v > 1) {
+ char buf[64];
+ if (c.exp == KEXP_FOREVER)
+ strcpy(buf, "forever");
+ else {
+ struct tm *tm;
+ const char *fmt;
+
+ if (fl & f_utc) {
+ tm = gmtime(&c.exp);
+ fmt = "%Y-%m-%d %H:%M:%S UTC";
+ } else {
+ tm = localtime(&c.exp);
+ fmt = "%Y-%m-%d %H:%M:%S %Z";
+ }
+ strftime(buf, sizeof(buf), fmt, tm);
+ }
+ printf("INFO keyid = %08lx; expiry = %s\n", (unsigned long)c.k, buf);
+ }
+
+ /* --- Check the authentication token width --- */
+
+ cbits = (d.len - COOKIE_SZ) * 8;
+ if (v > 2) printf("INFO authentication token width = %i bits\n", cbits);
+ if (bits == -1) {
+ if (cbits < minbits) {
+ if (v) printf("FAIL authentication token too narrow\n");
+ goto fail;
+ }
+ } else {
+ if (cbits != bits) {
+ if (v) printf("FAIL authentication token width doesn't match\n");
+ goto fail;
+ }
+ }
+ /* --- Get the key --- */
+
+ if ((k = key_byid(&f, c.k)) == 0) {
+ if (v) printf("FAIL keyid %08lx unavailable\n", (unsigned long)c.k);
+ goto fail;
+ }
+
+ /* --- Check that the cookie authenticates OK --- */
+
+ m = getmac(k, "cookie");
+ h = GM_INIT(m);
+ GH_HASH(h, d.buf, COOKIE_SZ);
+ if (argv[optind + 1])
+ GH_HASH(h, argv[optind + 1], strlen(argv[optind + 1]));
+ t = GH_DONE(h, 0);
+
+ if (memcmp(t, d.buf + COOKIE_SZ, cbits / 8) != 0) {
+ if (v) printf("FAIL bad authentication token\n");
+ goto fail;
+ }
+
+ /* --- See whether the cookie has expired --- */
+
+ if (c.exp == KEXP_FOREVER) {
+ if (!(fl & f_forever)) {
+ if (v) printf("FAIL forever cookies not allowed\n");
+ goto fail;
+ }
+ if (k->exp != KEXP_FOREVER) {
+ if (v) printf("FAIL cookie lasts forever but key will expire\n");
+ goto fail;
+ }
+ } else if (c.exp < now) {
+ if (v) printf("FAIL cookie has expired\n");
+ goto fail;
+ }
+
+ if (v) printf("OK\n");
+ key_close(&f);
+ GM_DESTROY(m);
+ GH_DESTROY(h);
+ dstr_destroy(&d);
+ return (0);
+
+fail:
+ key_close(&f);
+ dstr_destroy(&d);
+ return (1);
+
+#undef f_bogus
+#undef f_forever
+#undef f_utc
+}
+
+/*----- Main command table ------------------------------------------------*/
+
+static int cmd_help(int, char **);
+
+#define LISTS(LI) \
+ LI("Lists", list, \
+ listtab[i].name, listtab[i].name) \
+ LI("Message authentication algorithms", mac, \
+ gmactab[i], gmactab[i]->name)
+
+MAKELISTTAB(listtab, LISTS)
+
+static int cmd_show(int argc, char *argv[])
+{
+ return (displaylists(listtab, argv + 1));
+}
+
+static cmd cmds[] = {
+ { "help", cmd_help, "help [COMMAND...]" },
+ { "show", cmd_show, "show [ITEM...]" },
+ { "generate", cmd_gen,
+ "generate [-b BITS] [-e TIME] [-k TAG] [DATA]", "\
+Options:\n\
+\n\
+-b, --bits=N Use an N-bit token in the cookie.\n\
+-e, --expire=TIME Make the cookie expire after TIME.\n\
+-k, --key=TAG Use key TAG to create the token.\n\
+" },
+ { "verify", cmd_verify,
+ "verify [-fuqv] [-b BITS] [-m BITS] COOKIE [DATA]", "\
+Options:\n\
+\n\
+-b, --bits=N Accept tokens exactly N bits long only.\n\
+-m, --min-bits=N Accept tokens N bits long or more.\n\
+-f, --forever Accept cookies which never expire.\n\
+-u, --utc Output cookie expiry dates in UTC.\n\
+-q, --quiet Produce less output while checking cookies.\n\
+-v, --verbose Produce more output while checking cookies.\n\
+" },
+ { 0, 0, 0 }
+};
+
+static int cmd_help(int argc, char *argv[])
+{
+ sc_help(cmds, stdout, argv + 1);
+ return (0);
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- Helpful GNUy functions --- */
+
+static void usage(FILE *fp)
+{
+ fprintf(fp, "Usage: %s [-k KEYRING] COMMAND [ARGS]\n", QUIS);
+}
+
+void version(FILE *fp)
+{
+ fprintf(fp, "%s, Catacomb version " VERSION "\n", QUIS);
+}
+
+void help_global(FILE *fp)
+{
+ usage(fp);
+ fputs("\n\
+Generates and validates cryptographic cookies. Command line options\n\
+recognized are:\n\
+\n\
+-h, --help [COMMAND] Display this help text (or help for COMMAND).\n\
+-v, --version Display version number.\n\
+-u, --usage Display short usage summary.\n\
+\n\
+-k, --key-file=FILE Read and write keys in FILE.\n",
+ fp);
+}
+
+/* --- @main@ --- *
+ *
+ * Arguments: @int argc@ = number of command line arguments
+ * @char *argv[]@ = array of arguments
+ *
+ * Returns: Zero if OK, nonzero if not.
+ *
+ * Use: Generates and validates cryptographic cookies.
+ */
+
+int main(int argc, char *argv[])
+{
+ unsigned f = 0;
+
+#define f_bogus 1u
+#define f_forever 2u
+
+ /* --- Initialize the library --- */
+
+ ego(argv[0]);
+ sub_init();
+
+ /* --- Options parsing --- */
+
+ for (;;) {
+ static struct option opt[] = {
+
+ /* --- Standard GNUy help options --- */
+
+ { "help", 0, 0, 'h' },
+ { "version", 0, 0, 'v' },
+ { "usage", 0, 0, 'u' },
+
+ /* --- Actual relevant options --- */
+
+ { "keyring", OPTF_ARGREQ, 0, 'k' },
+
+ /* --- Magic terminator --- */
+
+ { 0, 0, 0, 0 }
+ };
+ int i = mdwopt(argc, argv, "+hvu k:", opt, 0, 0, 0);
+
+ if (i < 0)
+ break;
+ switch (i) {
+
+ /* --- Helpful GNUs --- */
+
+ case 'u':
+ usage(stdout);
+ exit(0);
+ case 'v':
+ version(stdout);
+ exit(0);
+ case 'h':
+ sc_help(cmds, stdout, argv + optind);
+ exit(0);
+
+ /* --- Real genuine useful options --- */
+
+ case 'k':
+ keyfile = optarg;
+ break;
+
+ /* --- Bogus things --- */
+
+ default:
+ f |= f_bogus;
+ break;
+ }
+ }
+
+ if ((f & f_bogus) || optind == argc) {
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ }
+
+ /* --- Dispatch to appropriate command handler --- */
+
+ argc -= optind;
+ argv += optind;
+ optind = 0;
+ return (findcmd(cmds, argv[0])->cmd(argc, argv));
+
+#undef f_bogus
+#undef f_forever
+}
+
+/*----- That's all, folks -------------------------------------------------*/