ktutil: Add import command and other improvements

- Add an import command that imports JSON as output by
   `ktutil list --json --keys`.

   This is enables one to filter/edit keytabs with jq!

 - Add a `merge` alias for the `copy` command, since that's effectively
   what it does.

 - Add a `--copy-duplicates` option to the `copy`/`merge` command.

 - Add a `--no-create` option to the `get` command.

 - Add a `--no-change-keys` option to the `get` command.

 - Make `add` complain if it can't finish writing to the keytab.
This commit is contained in:
Nicolas Williams
2022-10-01 17:57:54 -05:00
parent 69dc89b39a
commit ceec364ed4
5 changed files with 290 additions and 31 deletions

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997-2005 Kungliga Tekniska Högskolan
* Copyright (c) 1997-2022 Kungliga Tekniska Högskolan
* (Royal Institute of Technology, Stockholm, Sweden).
* All rights reserved.
*
@ -32,6 +32,8 @@
*/
#include "ktutil_locl.h"
#include <heimbase.h>
#include <base64.h>
RCSID("$Id$");
@ -153,6 +155,178 @@ kt_add(struct add_options *opt, int argc, char **argv)
krb5_warn(context, ret, "add");
out:
krb5_kt_free_entry(context, &entry);
krb5_kt_close(context, keytab);
if (ret == 0) {
ret = krb5_kt_close(context, keytab);
if (ret)
krb5_warn(context, ret, "Could not write the keytab");
} else {
krb5_kt_close(context, keytab);
}
return ret != 0;
}
/* We might be reading from a pipe, so we can't use rk_undumpdata() */
static char *
read_file(FILE *f)
{
size_t alloced;
size_t len = 0;
size_t bytes;
char *res, *end, *p;
if ((res = malloc(1024)) == NULL)
err(1, "Out of memory");
alloced = 1024;
end = res + alloced;
p = res;
do {
if (p == end) {
char *tmp;
if ((tmp = realloc(res, alloced + (alloced > 1))) == NULL)
err(1, "Out of memory");
alloced += alloced > 1;
p = tmp + (p - res);
res = tmp;
end = res + alloced;
}
bytes = fread(p, 1, end - p, f);
len += bytes;
p += bytes;
} while (bytes && !feof(f) && !ferror(f));
if (ferror(f))
errx(1, "Could not read all input");
if (p == end) {
char *tmp;
if ((tmp = strndup(res, len)) == NULL)
err(1, "Out of memory");
free(res);
res = tmp;
}
if (strlen(res) != len)
err(1, "Embedded NULs in input!");
return res;
}
static void
json2keytab_entry(heim_dict_t d, krb5_keytab kt, size_t idx)
{
krb5_keytab_entry e;
krb5_error_code ret;
heim_object_t v;
uint64_t u;
int64_t i;
char *buf = NULL;
memset(&e, 0, sizeof(e));
v = heim_dict_get_value(d, HSTR("timestamp"));
if (heim_get_tid(v) != HEIM_TID_NUMBER)
goto bad;
u = heim_number_get_long(v);
e.timestamp = u;
if (u != (uint64_t)e.timestamp)
goto bad;
v = heim_dict_get_value(d, HSTR("kvno"));
if (heim_get_tid(v) != HEIM_TID_NUMBER)
goto bad;
i = heim_number_get_long(v);
e.vno = i;
if (i != (int64_t)e.vno)
goto bad;
v = heim_dict_get_value(d, HSTR("enctype_number"));
if (heim_get_tid(v) != HEIM_TID_NUMBER)
goto bad;
i = heim_number_get_long(v);
e.keyblock.keytype = i;
if (i != (int64_t)e.keyblock.keytype)
goto bad;
v = heim_dict_get_value(d, HSTR("key"));
if (heim_get_tid(v) != HEIM_TID_STRING)
goto bad;
{
const char *s = heim_string_get_utf8(v);
int declen;
if ((buf = malloc(strlen(s))) == NULL)
err(1, "Out of memory");
declen = rk_base64_decode(s, buf);
if (declen < 0)
goto bad;
e.keyblock.keyvalue.data = buf;
e.keyblock.keyvalue.length = declen;
}
v = heim_dict_get_value(d, HSTR("principal"));
if (heim_get_tid(v) != HEIM_TID_STRING)
goto bad;
ret = krb5_parse_name(context, heim_string_get_utf8(v), &e.principal);
if (ret == 0)
ret = krb5_kt_add_entry(context, kt, &e);
/* For now, ignore aliases; besides, they're never set anywhere in-tree */
if (ret)
krb5_warn(context, ret,
"Could not parse or write keytab entry %lu",
(unsigned long)idx);
bad:
krb5_free_principal(context, e.principal);
}
int
kt_import(void *opt, int argc, char **argv)
{
krb5_error_code ret;
krb5_keytab kt;
heim_object_t o;
heim_error_t json_err = NULL;
heim_json_flags_t flags = HEIM_JSON_F_STRICT;
FILE *f = argc == 0 ? stdin : fopen(argv[0], "r");
size_t alen, i;
char *json;
if (f == NULL)
err(1, "Could not open file %s", argv[0]);
json = read_file(f);
o = heim_json_create(json, 10, flags, &json_err);
free(json);
if (o == NULL) {
if (json_err != NULL) {
o = heim_error_copy_string(json_err);
if (o)
errx(1, "Could not parse JSON: %s", heim_string_get_utf8(o));
}
errx(1, "Could not parse JSON");
}
if (heim_get_tid(o) != HEIM_TID_ARRAY)
errx(1, "JSON text must be an array");
alen = heim_array_get_length(o);
if (alen == 0)
errx(1, "Empty JSON array; not overwriting keytab");
if ((kt = ktutil_open_keytab()) == NULL)
err(1, "Could not open keytab");
for (i = 0; i < alen; i++) {
heim_object_t e = heim_array_get_value(o, i);
if (heim_get_tid(e) != HEIM_TID_DICT)
warnx("Element %ld of JSON text array is not an object", (long)i);
else
json2keytab_entry(heim_array_get_value(o, i), kt, i);
}
ret = krb5_kt_close(context, kt);
if (ret)
krb5_warn(context, ret, "Could not write the keytab");
return ret != 0;
}

@ -47,7 +47,7 @@ compare_keyblock(const krb5_keyblock *a, const krb5_keyblock *b)
}
int
kt_copy (void *opt, int argc, char **argv)
kt_copy (struct copy_options *opt, int argc, char **argv)
{
krb5_error_code ret;
krb5_keytab src_keytab, dst_keytab;
@ -106,11 +106,18 @@ kt_copy (void *opt, int argc, char **argv)
"already exists for %s, keytype %s, kvno %d",
name_str, etype_str, entry.vno);
}
krb5_kt_free_entry(context, &dummy);
krb5_kt_free_entry (context, &entry);
free(name_str);
free(etype_str);
continue;
if (!opt->copy_duplicates_flag) {
krb5_kt_free_entry(context, &dummy);
krb5_kt_free_entry (context, &entry);
free(name_str);
free(etype_str);
continue;
}
/*
* Because we can end up trying all keys that match the enctype,
* copying entries with duplicate principal, vno, and enctype, but
* different keys, can be useful.
*/
} else if(ret != KRB5_KT_NOTFOUND) {
krb5_warn (context, ret, "%s: fetching %s/%s/%u",
to, name_str, etype_str, entry.vno);

@ -197,23 +197,27 @@ kt_get(struct get_options *opt, int argc, char **argv)
break;
}
ret = kadm5_create_principal(kadm_handle, &princ, mask, "thisIs_aUseless.password123");
if(ret == 0)
created = 1;
else if(ret != KADM5_DUP) {
krb5_warn(context, ret, "kadm5_create_principal(%s)", argv[a]);
krb5_free_principal(context, princ_ent);
failed++;
continue;
}
ret = kadm5_randkey_principal_3(kadm_handle, princ_ent, keep, nks, ks,
&keys, &n_keys);
if (ret) {
krb5_warn(context, ret, "kadm5_randkey_principal(%s)", argv[a]);
krb5_free_principal(context, princ_ent);
failed++;
continue;
}
if (opt->create_flag) {
ret = kadm5_create_principal(kadm_handle, &princ, mask, "thisIs_aUseless.password123");
if(ret == 0)
created = 1;
else if(ret != KADM5_DUP) {
krb5_warn(context, ret, "kadm5_create_principal(%s)", argv[a]);
krb5_free_principal(context, princ_ent);
failed++;
continue;
}
}
if (opt->change_keys_flag) {
ret = kadm5_randkey_principal_3(kadm_handle, princ_ent, keep, nks, ks,
&keys, &n_keys);
if (ret) {
krb5_warn(context, ret, "kadm5_randkey_principal(%s)", argv[a]);
krb5_free_principal(context, princ_ent);
failed++;
continue;
}
}
ret = kadm5_get_principal(kadm_handle, princ_ent, &princ,
KADM5_PRINCIPAL | KADM5_KVNO | KADM5_ATTRIBUTES);

@ -1,5 +1,5 @@
/*
* Copyright (c) 2004 Kungliga Tekniska Högskolan
* Copyright (c) 2004-2022 Kungliga Tekniska Högskolan
* (Royal Institute of Technology, Stockholm, Sweden).
* All rights reserved.
*
@ -151,11 +151,17 @@ command = {
}
command = {
name = "copy"
name = "merge"
function = "kt_copy"
option = {
long = "copy-duplicates"
type = "flag"
help = "copy entries for the same principal and kvno, but different keys"
}
argument = "source destination"
min_args = "2"
max_args = "2"
help = "Copies one keytab to another."
help = "Merges one keytab into another."
}
command = {
name = "get"
@ -166,6 +172,16 @@ command = {
help = "admin principal"
argument = "principal"
}
option = {
long = "create"
type = "-flag"
help = "do not create the principal"
}
option = {
long = "change-keys"
type = "-flag"
help = "do not change the principal's keys"
}
option = {
long = "enctypes"
short = "e"
@ -214,6 +230,14 @@ command = {
argument = "principal..."
help = "Change keys for specified principals, and add them to the keytab."
}
command = {
name = "import"
function = "kt_import"
help = "Imports a keytab from JSON output of ktutil list --json --keys."
min_args = "0"
max_args = "1"
argument = "JSON-FILE"
}
command = {
name = "list"
option = {

@ -82,29 +82,67 @@ server for the realm of a keytab entry. Otherwise it will use the
values specified by the options.
.Pp
If no principals are given, all the ones in the keytab are updated.
.It Nm copy Ar keytab-src Ar keytab-dest
.It Nm copy Oo Fl Fl copy-duplicates Oc Ar keytab-src Ar keytab-dest
Copies all the entries from
.Ar keytab-src
to
.Ar keytab-dest .
Because entries already in
.Ar keytab-dest
are kept, this command functions to merge keytabs.
Entries for the same principal, key version number, and
encryption type in the
.Ar keytab-src
that are also in the
.Ar keytab-dest
will not be copied to the
.Ar keytab-dest
unless the
.Fl Fl copy-duplicates
option is given.
.It Nm get Oo Fl p Ar admin principal Oc \
Oo Fl Fl principal= Ns Ar admin principal Oc Oo Fl e Ar enctype Oc \
Oo Fl Fl no-create Oc \
Oo Fl Fl no-change-keys Oc \
Oo Fl Fl keepold | Fl Fl keepallold | Fl Fl pruneall Oc \
Oo Fl Fl enctypes= Ns Ar enctype Oc Oo Fl r Ar realm Oc \
Oo Fl Fl realm= Ns Ar realm Oc Oo Fl a Ar admin server Oc \
Oo Fl Fl admin-server= Ns Ar admin server Oc Oo Fl s Ar server port Oc \
Oo Fl Fl server-port= Ns Ar server port Oc Ar principal ...
.Pp
For each
.Ar principal ,
generate a new key for it (creating it if it doesn't already exist),
and put that key in the keytab.
get a the principal's keys from the KDC via the kadmin protocol,
creating the principal if it doesn't exist (unless
.Fl Fl no-create
is given), and changing its keys to new random keys (unless
.Fl Fl no-change-keys
is given).
.Pp
If no
.Ar realm
is specified, the realm to operate on is taken from the first
principal.
.It Nm import Oo JSON-FILE Oc
Read an array of keytab entries in a JSON file and copy them to
the keytab.
Use the
.Nm list
command with its
.Fl Fl json
option
and
.Fl Fl keys
option to export a keytab.
.It Nm list Oo Fl Fl keys Oc Op Fl Fl timestamp Oo Op Fl Fl json Oc
List the keys stored in the keytab.
Use the
.Fl Fl json
and
.Fl Fl keys
options to export a keytab as JSON for importing with the
.Nm import
command.
.It Nm remove Oo Fl p Ar principal Oc Oo Fl Fl principal= Ns Ar principal Oc \
Oo Fl V kvno Oc Oo Fl Fl kvno= Ns Ar kvno Oc Oo Fl e enctype Oc \
Oo Fl Fl enctype= Ns Ar enctype Oc
@ -113,8 +151,14 @@ Removes the specified key or keys. Not specifying a
removes keys with any version number. Not specifying an
.Ar enctype
removes keys of any type.
.It Nm merge Oo Fl Fl copy-duplicates Oc Ar keytab-src Ar keytab-dest
An alias for the
.Nm copy
command.
.It Nm rename Ar from-principal Ar to-principal
Renames all entries in the keytab that match the
Renames all entries for the
.Ar from-principal
in the keytab
.Ar from-principal
to
.Ar to-principal .
@ -123,6 +167,12 @@ Removes all old versions of a key for which there is a newer version
that is at least
.Ar age
(default one week) old.
Note that this does not update the KDC database.
The
.Xr kadmin 1
command has a
.Nm prune
command that can do this on the KDC side.
.El
.Sh SEE ALSO
.Xr kadmin 1