From 07ce06e7b2cde15772455803888cdf79a44490e1 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 21 Dec 2022 17:57:41 -0600 Subject: [PATCH] kadm5: Add test_marshall program The lib/kadm5/test_marshall program allows one to construct and check encodings for various struct types for which we have {kadm5,krb5}_{ret,store}_() functions. Currently supported are: - krb5_keyblock - krb5_principal - krb5_times - krb5_address - krb5_addresses - krb5_authdata - krb5_creds - krb5_key_data - krb5_tl_data - kadm5_principal_ent_rec With this we'll be able to a) construct test vectors, b) use those to drive fuzzing with AFL or other fuzzers. --- lib/kadm5/Makefile.am | 5 +- lib/kadm5/marshall.c | 457 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+), 1 deletion(-) diff --git a/lib/kadm5/Makefile.am b/lib/kadm5/Makefile.am index a6151d51b..6b58c5f8f 100644 --- a/lib/kadm5/Makefile.am +++ b/lib/kadm5/Makefile.am @@ -15,7 +15,7 @@ libkadm5srv_la_LDFLAGS += $(LDFLAGS_VERSION_SCRIPT)$(srcdir)/version-script.map endif sbin_PROGRAMS = iprop-log -check_PROGRAMS = default_keys +check_PROGRAMS = default_keys test_marshall noinst_PROGRAMS = test_pw_quality noinst_LTLIBRARIES = sample_passwd_check.la sample_hook.la @@ -37,6 +37,9 @@ libexec_PROGRAMS = ipropd-master ipropd-slave default_keys_SOURCES = default_keys.c default_keys_CPPFLAGS = -I$(srcdir)/../krb5 +test_marshall_SOURCES = marshall.c +test_marshall_CPPFLAGS = -I$(srcdir)/../krb5 -DTEST + kadm5includedir = $(includedir)/kadm5 buildkadm5include = $(buildinclude)/kadm5 diff --git a/lib/kadm5/marshall.c b/lib/kadm5/marshall.c index 37c497ba3..e39d6924e 100644 --- a/lib/kadm5/marshall.c +++ b/lib/kadm5/marshall.c @@ -472,3 +472,460 @@ _kadm5_unmarshal_params(krb5_context context, return ret; } + +#ifdef TEST +#include +#include +#include + +static int version_flag; +static int help_flag; +static int verbose_flag; +static int in_text_flag = 0; +static int in_binary_flag = 0; +static int out_hex_flag = 0; +static int out_binary_flag = 0; +static int must_round_trip_flag = 0; +static int byteorder_packed_flag = 0; +static char *byteorder_string_in_string; +static char *byteorder_string_out_string; +static struct getargs args[] = { + { "version", '\0', arg_flag, &version_flag, + "Version", NULL }, + { "help", '\0', arg_flag, &help_flag, + "Show this message", NULL }, + { "verbose", 'v', arg_flag, &verbose_flag, NULL, NULL }, + { "in-text", '\0', arg_flag, &in_text_flag, + "Input is a text \"recipe\"", NULL }, + { "in-binary", '\0', arg_flag, &in_binary_flag, + "Input is binary", NULL }, + { "out-hex", '\0', arg_flag, &out_hex_flag, + "Output hex", NULL }, + { "out-binary", '\0', arg_flag, &out_binary_flag, + "Output binary", NULL }, + { "must-round-trip", '\0', arg_flag, &must_round_trip_flag, + "Check that encoding and decoding round-trip", NULL }, + { "byte-order-out", '\0', arg_string, &byteorder_string_out_string, + "Output byte order", "host, network, be, or le" }, + { "byte-order-in", '\0', arg_string, &byteorder_string_in_string, + "Input byte order", "host, network, be, or le" }, + { "byte-order-packed", '\0', arg_flag, &byteorder_packed_flag, NULL, NULL }, +}; + +#define DO_TYPE1(t, r, s) \ + if (strcmp(type, #t) == 0) { \ + t v; \ + ret = r(in, &v); \ + if (ret == 0) \ + ret = s(out, v); \ + return ret; \ + } + +#define DO_TYPE2(t, r, s) \ + if (strcmp(type, #t) == 0) { \ + t v; \ + ret = r(in, &v); \ + if (ret == 0) \ + ret = s(out, &v); \ + return ret; \ + } + +static krb5_error_code +reencode(const char *type, krb5_storage *in, krb5_storage *out) +{ + krb5_error_code ret; + + krb5_storage_seek(in, 0, SEEK_SET); + + /* + * TODO: When --verbose print a visual representation of the value. + * + * We have functionality in lib/krb5 for that for krb5_principal and + * krb5_address, but not any of the others. Adding krb5_print_*() + * and kadm5_print_*() functions just for this program to use seems + * annoying. + */ + DO_TYPE1(krb5_keyblock, krb5_ret_keyblock, krb5_store_keyblock); + DO_TYPE1(krb5_principal, krb5_ret_principal, krb5_store_principal); + DO_TYPE1(krb5_times, krb5_ret_times, krb5_store_times); + DO_TYPE1(krb5_address, krb5_ret_address, krb5_store_address); + DO_TYPE1(krb5_addresses, krb5_ret_addrs, krb5_store_addrs); + DO_TYPE1(krb5_authdata, krb5_ret_authdata, krb5_store_authdata); + + DO_TYPE2(krb5_creds, krb5_ret_creds, krb5_store_creds); + DO_TYPE2(krb5_key_data, kadm5_ret_key_data, kadm5_store_key_data); + DO_TYPE2(krb5_tl_data, kadm5_ret_tl_data, kadm5_store_tl_data); + DO_TYPE2(kadm5_principal_ent_rec, kadm5_ret_principal_ent, + kadm5_store_principal_ent); + + return ENOTSUP; +} + +static krb5_error_code +eval_recipe1(krb5_storage *sp, const char *typ, const char *val) +{ + krb5_error_code ret; + uint64_t vu = 0; + int64_t vi = 0; + int consumed = 0; + + if (strncmp(typ, "int", sizeof("int") - 1) == 0) { + if (sscanf(val, "%"PRIi64"%n", &vi, &consumed) != 1) + return EINVAL; + if (consumed < 1) + return EINVAL; + while (isspace(val[consumed])) + consumed++; + if (val[consumed] != '\0') + return EINVAL; + } else if (strncmp(typ, "uint", sizeof("uint") - 1) == 0) { + /* There's no equally-useful equivalent of %i for unsigned */ + if (val[0] == '0') { + if (val[1] == 'x') { + if (sscanf(val, "%"PRIx64"%n", &vu, &consumed) != 1) + return EINVAL; + } else { + if (sscanf(val, "%"PRIo64"%n", &vu, &consumed) != 1) + return EINVAL; + } + } else { + if (sscanf(val, "%"PRIu64"%n", &vu, &consumed) != 1) + return EINVAL; + } + if (consumed < 1) + return EINVAL; + while (isspace(val[consumed])) + consumed++; + if (val[consumed] != '\0') + return EINVAL; + vi = (int64_t)vu; + } +#define DO_INTn(n) \ + if (strcmp(typ, "int" #n) == 0) { \ + if (n < 64 && vi < INT ## n ## _MIN) \ + return EOVERFLOW; \ + if (n < 64 && vi > INT ## n ## _MAX) \ + return EOVERFLOW; \ + return krb5_store_int ## n (sp, vi); \ + } + DO_INTn(8); + DO_INTn(16); + DO_INTn(32); + DO_INTn(64); +#define DO_UINTn(n) \ + if (strcmp(typ, "uint" #n) == 0) { \ + if (n < 64 && vu > INT ## n ## _MAX) \ + return EOVERFLOW; \ + return krb5_store_int ## n (sp, vi); \ + } + DO_UINTn(8); + DO_UINTn(16); + DO_UINTn(32); + DO_UINTn(64); + if (strcmp(typ, "string") == 0) + return krb5_store_string(sp, val); + if (strcmp(typ, "stringz") == 0) + return krb5_store_stringz(sp, val); + if (strcmp(typ, "stringnl") == 0) + return krb5_store_stringnl(sp, val); + if (strcmp(typ, "data") == 0) { + ssize_t dsz = strlen(val); + krb5_data d; + + /* + * 'data' as in 'krb5_data'. + * + * krb5_store_data() stores the length then the data. + */ + if (krb5_data_alloc(&d, dsz)) + return ENOMEM; + dsz = hex_decode(val, d.data, d.length); + if (dsz < 0) + return EINVAL; + d.length = dsz; + ret = krb5_store_data(sp, d); + krb5_data_free(&d); + return ret; + } + if (strcmp(typ, "rawdata") == 0) { + ssize_t dsz = strlen(val); + void *d; + + /* Store the data w/o a length prefix */ + d = malloc(dsz); + if (d == NULL) + return ENOMEM; + dsz = hex_decode(val, d, dsz); + if (dsz < 0) + return EINVAL; + ret = krb5_store_datalen(sp, d, dsz); + free(d); + return ret; + } + return ENOTSUP; +} + +static krb5_storage * +eval_recipe(char *r, int spflags) +{ + krb5_error_code ret; + krb5_storage *sp; + unsigned int lineno = 0; + char *nxt = NULL; + char *p; + + sp = krb5_storage_emem(); + if (sp == NULL) + errx(1, "Out of memory"); + krb5_storage_set_flags(sp, spflags); + + for (p = r; p && *p; p = nxt) { + char *typ; + char *val; + + lineno++; + + /* Terminate p at \n */ + nxt = p; + do { + nxt = strpbrk(nxt, "\r\n"); + if (nxt && *nxt == '\r') { + if (*(++nxt) != '\n') + continue; + } + if (nxt && *nxt == '\n') { + *(nxt++) = '\0'; + break; + } + } while (nxt); + + while (isspace(*p)) + p++; + if (*p == '#') { + p = nxt; + continue; + } + if (*p == '\0') + continue; + typ = p; + val = strpbrk(p, " \t"); + if (val) { + *(val++) = '\0'; + while (isspace(*val)) + val++; + } + ret = eval_recipe1(sp, typ, val); + if (ret) + krb5_err(NULL, 1, ret, "Error at line %u", lineno); + } + return sp; +} + +static void +usage(int code) +{ + if (code) + dup2(STDERR_FILENO, STDOUT_FILENO); + + arg_printusage(args, sizeof(args) / sizeof(args[0]), "test_marshall", + "Usage: test_marshal [options] TYPE-NAME INPUT-FILE " + "[OUTPUT-FILE]\n" + "\tText inputs must be of the form:\n\n" + "\t\tsimpletype literalvalue\n\n" + "\twhere {simpletype} is one of:\n\n" + "\t\tint8\n" + "\t\tint16\n" + "\t\tint32\n" + "\t\tint64\n" + "\t\tuint8\n" + "\t\tuint16\n" + "\t\tuint32\n" + "\t\tuint64\n" + "\t\tstring\n" + "\t\tstringz\n" + "\t\tstringnl\n" + "\t\tdata\n" + "\t\trawdata\n\n" + "\tand {literalvalue} is as appropriate for the {simpletype}:\n\n" + "\t - For int types the value can be decimal, octal, or hexadecimal.\n" + "\t - For string types the string ends at the end of the line.\n" + "\t - For {data} the value is hex and will be encoded as a 32-bit\n" + "\t length then the raw binary data.\n" + "\t - For {rawdata} the value is hex and will be encoded as just the\n" + "\t raw binary data.\n\n" + "\tThe {TYPE} must be one of: krb5_keyblock, krb5_principal,\n" + "\tkrb5_times, krb5_address, krb5_addresses, krb5_authdata,\n" + "\tkrb5_creds, krb5_key_data, krb5_tl_data, or\n" + "\tkadm5_principal_ent_rec.\n\n" + "Options:\n"); + exit(code); +} + +static krb5_flags +byteorder_flags(const char *s) +{ + if (s == NULL) + return KRB5_STORAGE_BYTEORDER_HOST; + if (strcasecmp(s, "host") == 0) + return KRB5_STORAGE_BYTEORDER_HOST; + if (strcasecmp(s, "network") == 0) + return KRB5_STORAGE_BYTEORDER_BE; + if (strcasecmp(s, "be") == 0) + return KRB5_STORAGE_BYTEORDER_BE; + if (strcasecmp(s, "le") == 0) + return KRB5_STORAGE_BYTEORDER_LE; + return 0; +} + +/* + * This program is intended to make fuzzing of krb5_ret_*() and kadm5_ret_*() + * possible. + * + * Inputs are either binary encodings or simplistic textual representations of + * XDR-ish data structures normally coded with {kadm5,krb5}_{ret,store}_*() + * functions. + * + * A textual representation of these structures looks like: + * + * type value + * .. + * + * where type is one of char, int32, etc., and where value is an appropriate + * literal for type. + */ +int +main(int argc, char **argv) +{ + krb5_error_code ret = 0; + krb5_storage *insp = NULL; + krb5_storage *outsp = NULL; + krb5_flags spflags_in = 0; + krb5_flags spflags_out = 0; + krb5_data i, o; + size_t insz = 0; + char *intxt = NULL; + void *inbin = NULL; + int optidx = 0; + + if (getarg(args, sizeof(args)/sizeof(args[0]), argc, argv, &optidx)) + usage(1); + + if (help_flag) + usage(0); + + argc -= optidx; + argv += optidx; + + if (argc < 1) + errx(1, "Missing type name argument"); + if (argc < 2) + errx(1, "Missing input file argument"); + if (argc > 3) + errx(1, "Too many arguments"); + + if ((in_text_flag && in_binary_flag) || + (!in_text_flag && !in_binary_flag)) + errx(1, "One and only one of --in-text and --in-binary must be given"); + if (out_hex_flag && out_binary_flag) + errx(1, "At most one of --out-text and --out-binary must be given"); + + if (!out_hex_flag && !out_binary_flag) { + if (isatty(STDOUT_FILENO)) { + warnx("Will output hex because stdout is a terminal"); + out_hex_flag = 1; + } else { + warnx("Will output binary"); + out_binary_flag = 1; + } + } + + if (byteorder_packed_flag) { + spflags_in |= KRB5_STORAGE_BYTEORDER_PACKED; + spflags_out |= KRB5_STORAGE_BYTEORDER_PACKED; + } + spflags_in |= byteorder_flags(byteorder_string_in_string); + spflags_out |= byteorder_flags(byteorder_string_out_string); + + /* Read the input */ + if (in_text_flag) + errno = rk_undumptext(argv[1], &intxt, NULL); + else + errno = rk_undumpdata(argv[1], &inbin, &insz); + if (errno) + err(1, "Could not read %s", argv[1]); + + /* If the input is a recipe, evaluate it */ + if (intxt) + insp = eval_recipe(intxt, spflags_in); + else + insp = krb5_storage_from_mem(inbin, insz); + if (insp == NULL) + errx(1, "Out of memory"); + krb5_storage_set_flags(insp, spflags_in); + + outsp = krb5_storage_emem(); + if (outsp == NULL) + errx(1, "Out of memory"); + krb5_storage_set_flags(outsp, spflags_out); + + ret = reencode(argv[0], insp, outsp); + if (ret) + krb5_err(NULL, 1, ret, "Could not decode and re-encode"); + + ret = krb5_storage_to_data(insp, &i); + if (ret == 0) + ret = krb5_storage_to_data(outsp, &o); + if (ret) + krb5_err(NULL, 1, ret, "Could not check round-tripping"); + if (i.length != o.length || memcmp(i.data, o.data, i.length) != 0) { + if (must_round_trip_flag) { + char *hexstr = NULL; + + if (hex_encode(inbin, insz, &hexstr) == -1) + err(1, "Encoding does not round-trip"); + if (fprintf(stderr, "%s\n", hexstr) < 0) + err(1, "Could not output encoding"); + free(hexstr); + errx(1, "Encoding does not round-trip"); + } else { + warnx("Encoding does not round-trip"); + } + } else if (verbose_flag) { + fprintf(stderr, "Encoding round-trips!\n"); + } + + if (out_hex_flag) { + char *hexstr = NULL; + + if (hex_encode(i.data, i.length, &hexstr) == -1) + err(1, "Could not hex-encode output"); + if (argv[2]) { + FILE *f; + + f = fopen(argv[2], "w"); + if (f == NULL) + err(1, "Could not open %s for writing", argv[2]); + if (fprintf(f, "%s\n", hexstr) < 0 || fclose(f)) + err(1, "Could write to %s", argv[2]); + } else { + if (printf("%s\n", hexstr) < 0) + err(1, "Could not write to stdout"); + } + free(hexstr); + } else { + if (argv[2]) { + rk_dumpdata(argv[2], i.data, i.length); + } else { + if (fwrite(i.data, i.length, 1, stdout) != 1 || + fflush(stdout) != 0) + err(1, "Could not output encoding"); + } + } + + krb5_data_free(&o); + krb5_data_free(&i); + krb5_storage_free(insp); + krb5_storage_free(outsp); + return ret; +} +#endif