/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* plugins/kdb/test/kdb_test.c - Test KDB module */
/*
* Copyright (C) 2015 by the Massachusetts Institute of Technology.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* This is a read-only KDB module intended to help test KDC behavior which
* cannot be exercised with the DB2 module. Responses are read from the
* dbmodules subsection according to this example:
*
* [dbmodules]
* test = {
* alias = {
* aliasname = canonname
* # For cross-realm aliases, only the realm part will
* # matter to the client.
* aliasname = @FOREIGN_REALM
* enterprise@PRINC = @FOREIGN_REALM
* }
* princs = {
* krbtgt/KRBTEST.COM = {
* flags = +preauth +ok-to-auth-as-delegate
* maxlife = 1d
* maxrenewlife = 7d
* expiration = 14d # relative to current time
* pwexpiration = 1h
* # Initial number is kvno; defaults to 1.
* keys = 3 aes256-cts aes128-cts:normal
* keys = 2 rc4-hmac
* strings = key1:value1
* strings = key2:value2
* }
* }
* delegation = {
* # Traditional constrained delegation; target_service
* # must be in the same realm.
* intermediate_service = target_service
* }
* rbcd = {
* # Resource-based constrained delegation;
* # intermediate_service may be in a different realm.
* target_service = intermediate_service
* }
* }
*
* Key values are generated using a hash of the kvno, enctype, salt type,
* principal name, and lookup realm. This module does not use master key
* encryption, so it serves as a partial test of the DAL's ability to avoid
* that.
*
* Inbound cross-realm TGT entries are currently implicit; they will use the
* same configuration and key enctypes as the local krbtgt principal, although
* they will use different keys (because the lookup realm is hashed in).
* Outgoing cross-realm TGT entries must be added explicitly
* (krbtgt/OTHER_REALM).
*/
#include "k5-int.h"
#include "kdb5.h"
#include "adm_proto.h"
#include <ctype.h>
#define TEST_AD_TYPE -456
#define IS_TGS_PRINC(p) ((p)->length == 2 && \
data_eq_string((p)->data[0], KRB5_TGS_NAME))
typedef struct {
void *profile;
char *section;
const char *names[6];
} *testhandle;
static void *
ealloc(size_t sz)
{
void *p = calloc(sz, 1);
if (p == NULL)
abort();
return p;
}
static char *
estrdup(const char *s)
{
char *copy = strdup(s);
if (copy == NULL)
abort();
return copy;
}
static void
check(krb5_error_code code)
{
if (code != 0)
abort();
}
/* Set up for a profile query using h->names. Look up s1 -> s2 -> s3 (some of
* which may be NULL) within this database's dbmodules section. */
static void
set_names(testhandle h, const char *s1, const char *s2, const char *s3)
{
h->names[0] = KDB_MODULE_SECTION;
h->names[1] = h->section;
h->names[2] = s1;
h->names[3] = s2;
h->names[4] = s3;
h->names[5] = NULL;
}
/* Look up a string within this database's dbmodules section. */
static char *
get_string(testhandle h, const char *s1, const char *s2, const char *s3)
{
krb5_error_code ret;
char **values, *val;
set_names(h, s1, s2, s3);
ret = profile_get_values(h->profile, h->names, &values);
if (ret == PROF_NO_RELATION)
return NULL;
if (ret)
abort();
val = estrdup(values[0]);
profile_free_list(values);
return val;
}
/* Look up a duration within this database's dbmodules section. */
static krb5_deltat
get_duration(testhandle h, const char *s1, const char *s2, const char *s3)
{
char *strval = get_string(h, s1, s2, s3);
krb5_deltat val;
if (strval == NULL)
return 0;
check(krb5_string_to_deltat(strval, &val));
free(strval);
return val;
}
/* Look up an absolute time within this database's dbmodules section. The time
* is expressed in the profile as an interval relative to the current time. */
static krb5_timestamp
get_time(testhandle h, const char *s1, const char *s2, const char *s3)
{
char *strval = get_string(h, s1, s2, s3);
krb5_deltat val;
if (strval == NULL)
return 0;
check(krb5_string_to_deltat(strval, &val));
free(strval);
return val + time(NULL);
}
/* Initialize kb_out with a key of type etype, using a hash of kvno, etype,
* salttype, and princstr for the key bytes. */
static void
make_keyblock(krb5_kvno kvno, krb5_enctype etype, int32_t salttype,
const char *princstr, const krb5_data *realm,
krb5_keyblock *kb_out)
{
size_t keybytes, keylength, pos, n;
char *hashstr;
krb5_data d, rndin;
krb5_checksum cksum;
check(krb5_c_keylengths(NULL, etype, &keybytes, &keylength));
alloc_data(&rndin, keybytes);
/* Hash the kvno, enctype, salt type, and principal name together. */
if (asprintf(&hashstr, "%d %d %d %s %.*s", (int)kvno, (int)etype,
(int)salttype, princstr, (int)realm->length, realm->data) < 0)
abort();
d = string2data(hashstr);
check(krb5_c_make_checksum(NULL, CKSUMTYPE_NIST_SHA, NULL, 0, &d, &cksum));
/* Make the appropriate number of input bytes from the hash result. */
for (pos = 0; pos < keybytes; pos += n) {
n = (cksum.length < keybytes - pos) ? cksum.length : keybytes - pos;
memcpy(rndin.data + pos, cksum.contents, n);
}
kb_out->enctype = etype;
kb_out->length = keylength;
kb_out->contents = ealloc(keylength);
check(krb5_c_random_to_key(NULL, etype, &rndin, kb_out));
free(cksum.contents);
free(rndin.data);
free(hashstr);
}
/* Return key data for the given key/salt tuple strings, using hashes of the
* enctypes, salts, and princstr for the key contents. */
static void
make_keys(char **strings, const char *princstr, const krb5_data *realm,
krb5_db_entry *ent)
{
krb5_key_data *key_data, *kd;
krb5_keyblock kb;
int32_t *ks_list_sizes, nstrings, nkeys, i, j;
krb5_key_salt_tuple **ks_lists, *ks;
krb5_kvno *kvnos;
char *s;
for (nstrings = 0; strings[nstrings] != NULL; nstrings++);
ks_lists = ealloc(nstrings * sizeof(*ks_lists));
ks_list_sizes = ealloc(nstrings * sizeof(*ks_list_sizes));
kvnos = ealloc(nstrings * sizeof(*kvnos));
/* Convert each string into a key/salt tuple list and count the total
* number of key data structures needed. */
nkeys = 0;
for (i = 0; i < nstrings; i++) {
s = strings[i];
/* Read a leading kvno if present; otherwise assume kvno 1. */
if (isdigit(*s)) {
kvnos[i] = strtol(s, &s, 10);
while (isspace(*s))
s++;
} else {
kvnos[i] = 1;
}
check(krb5_string_to_keysalts(s, NULL, NULL, FALSE, &ks_lists[i],
&ks_list_sizes[i]));
nkeys += ks_list_sizes[i];
}
/* Turn each key/salt tuple into a key data entry. */
kd = key_data = ealloc(nkeys * sizeof(*kd));
for (i = 0; i < nstrings; i++) {
ks = ks_lists[i];
for (j = 0; j < ks_list_sizes[i]; j++) {
make_keyblock(kvnos[i], ks[j].ks_enctype, ks[j].ks_salttype,
princstr, realm, &kb);
kd->key_data_ver = 2;
kd->key_data_kvno = kvnos[i];
kd->key_data_type[0] = ks[j].ks_enctype;
kd->key_data_length[0] = kb.length;
kd->key_data_contents[0] = kb.contents;
kd->key_data_type[1] = ks[j].ks_salttype;
kd++;
}
}
for (i = 0; i < nstrings; i++)
free(ks_lists[i]);
free(ks_lists);
free(ks_list_sizes);
free(kvnos);
ent->key_data = key_data;
ent->n_key_data = nkeys;
}
static void
make_strings(char **stringattrs, krb5_db_entry *ent)
{
struct k5buf buf;
char **p;
const char *str, *sep;
krb5_tl_data *tl;
k5_buf_init_dynamic(&buf);
for (p = stringattrs; *p != NULL; p++) {
str = *p;
sep = strchr(str, ':');
assert(sep != NULL);
k5_buf_add_len(&buf, str, sep - str);
k5_buf_add_len(&buf, "\0", 1);
k5_buf_add_len(&buf, sep + 1, strlen(sep + 1) + 1);
}
assert(buf.data != NULL);
tl = ealloc(sizeof(*ent->tl_data));
tl->tl_data_next = NULL;
tl->tl_data_type = KRB5_TL_STRING_ATTRS;
tl->tl_data_length = buf.len;
tl->tl_data_contents = buf.data;
ent->tl_data = tl;
}
static krb5_error_code
test_init()
{
return 0;
}
static krb5_error_code
test_cleanup()
{
return 0;
}
static krb5_error_code
test_open(krb5_context context, char *conf_section, char **db_args, int mode)
{
testhandle h;
h = ealloc(sizeof(*h));
h->profile = context->profile;
h->section = estrdup(conf_section);
context->dal_handle->db_context = h;
return 0;
}
static krb5_error_code
test_close(krb5_context context)
{
testhandle h = context->dal_handle->db_context;
free(h->section);
free(h);
return 0;
}
/* Return the principal name krbtgt/tgs_realm@our_realm. */
static krb5_principal
tgtname(krb5_context context, const krb5_data *tgs_realm,
const krb5_data *our_realm)
{
krb5_principal princ;
check(krb5_build_principal_ext(context, &princ,
our_realm->length, our_realm->data,
KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME,
tgs_realm->length, tgs_realm->data, 0));
princ->type = KRB5_NT_SRV_INST;
return princ;
}
static krb5_error_code
test_get_principal(krb5_context context, krb5_const_principal search_for,
unsigned int flags, krb5_db_entry **entry)
{
krb5_error_code ret;
krb5_principal princ = NULL, tgtprinc;
krb5_principal_data empty_princ = { KV5M_PRINCIPAL };
testhandle h = context->dal_handle->db_context;
char *search_name = NULL, *canon = NULL, *flagstr;
char **names, **key_strings, **stringattrs;
const char *ename;
krb5_db_entry *ent;
*entry = NULL;
check(krb5_unparse_name_flags(context, search_for,
KRB5_PRINCIPAL_UNPARSE_NO_REALM,
&search_name));
canon = get_string(h, "alias", search_name, NULL);
if (canon != NULL) {
check(krb5_parse_name(context, canon, &princ));
if (!krb5_realm_compare(context, search_for, princ)) {
/* Out of realm */
if ((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) &&
((flags & KRB5_KDB_FLAG_CANONICALIZE) ||
search_for->type == KRB5_NT_ENTERPRISE_PRINCIPAL)) {
/* Return a client referral by creating an entry with only the
* principal set. */
*entry = ealloc(sizeof(**entry));
(*entry)->princ = princ;
princ = NULL;
ret = 0;
goto cleanup;
} else if (flags & KRB5_KDB_FLAG_CANONICALIZE) {
/* Generate a server referral by looking up the TGT for the
* canonical name's realm. */
tgtprinc = tgtname(context, &princ->realm, &search_for->realm);
krb5_free_principal(context, princ);
princ = tgtprinc;
krb5_free_unparsed_name(context, search_name);
check(krb5_unparse_name_flags(context, princ,
KRB5_PRINCIPAL_UNPARSE_NO_REALM,
&search_name));
ename = search_name;
} else {
ret = KRB5_KDB_NOENTRY;
goto cleanup;
}
} else {
ename = canon;
}
} else {
check(krb5_copy_principal(context, search_for, &princ));
ename = search_name;
}
/* Check that the entry exists. */
set_names(h, "princs", ename, NULL);
ret = profile_get_relation_names(h->profile, h->names, &names);
if (ret == PROF_NO_RELATION) {
ret = KRB5_KDB_NOENTRY;
goto cleanup;
}
profile_free_list(names);
/* No error exits after this point. */
ent = ealloc(sizeof(*ent));
ent->princ = princ;
princ = NULL;
flagstr = get_string(h, "princs", ename, "flags");
if (flagstr != NULL) {
check(krb5_flagspec_to_mask(flagstr, &ent->attributes,
&ent->attributes));
}
free(flagstr);
ent->max_life = get_duration(h, "princs", ename, "maxlife");
ent->max_renewable_life = get_duration(h, "princs", ename, "maxrenewlife");
ent->expiration = get_time(h, "princs", ename, "expiration");
ent->pw_expiration = get_time(h, "princs", ename, "pwexpiration");
/* Leave last_success, last_failed, fail_auth_count zeroed. */
/* Leave e_data empty. */
set_names(h, "princs", ename, "keys");
ret = profile_get_values(h->profile, h->names, &key_strings);
if (ret != PROF_NO_RELATION) {
make_keys(key_strings, ename, &search_for->realm, ent);
profile_free_list(key_strings);
}
set_names(h, "princs", ename, "strings");
ret = profile_get_values(h->profile, h->names, &stringattrs);
if (ret != PROF_NO_RELATION) {
make_strings(stringattrs, ent);
profile_free_list(stringattrs);
}
/* We must include mod-princ data or kadm5_get_principal() won't work and
* we can't extract keys with kadmin.local. */
check(krb5_dbe_update_mod_princ_data(context, ent, 0, &empty_princ));
*entry = ent;
ret = 0;
cleanup:
krb5_free_unparsed_name(context, search_name);
krb5_free_principal(context, princ);
free(canon);
return ret;
}
static void
lookup_princ_by_cert(krb5_context context, const krb5_data *client_cert,
krb5_principal *princ)
{
krb5_error_code ret;
char *cert_princ_name;
/* The test client sends a principal string instead of a cert. */
cert_princ_name = k5memdup0(client_cert->data, client_cert->length, &ret);
check(ret);
check(krb5_parse_name(context, cert_princ_name, princ));
free(cert_princ_name);
}
static krb5_error_code
test_get_s4u_x509_principal(krb5_context context, const krb5_data *client_cert,
krb5_const_principal princ, unsigned int flags,
krb5_db_entry **entry)
{
krb5_error_code ret;
krb5_principal cert_princ, canon_princ;
testhandle h = context->dal_handle->db_context;
krb5_boolean match;
char *canon, *princ_name;
lookup_princ_by_cert(context, client_cert, &cert_princ);
ret = test_get_principal(context, cert_princ, flags, entry);
krb5_free_principal(context, cert_princ);
if (ret || (flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY))
return ret;
if (!krb5_realm_compare(context, princ, (*entry)->princ))
abort();
if (princ->length == 0 ||
krb5_principal_compare(context, princ, (*entry)->princ))
return 0;
match = FALSE;
check(krb5_unparse_name_flags(context, princ,
KRB5_PRINCIPAL_UNPARSE_NO_REALM,
&princ_name));
canon = get_string(h, "alias", princ_name, NULL);
krb5_free_unparsed_name(context, princ_name);
if (canon != NULL) {
check(krb5_parse_name(context, canon, &canon_princ));
match = krb5_principal_compare(context, canon_princ, (*entry)->princ);
krb5_free_principal(context, canon_princ);
}
free(canon);
return match ? 0 : KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
}
static krb5_error_code
test_fetch_master_key(krb5_context context, krb5_principal mname,
krb5_keyblock *key_out, krb5_kvno *kvno_out,
char *db_args)
{
memset(key_out, 0, sizeof(*key_out));
*kvno_out = 0;
return 0;
}
static krb5_error_code
test_fetch_master_key_list(krb5_context context, krb5_principal mname,
const krb5_keyblock *key,
krb5_keylist_node **mkeys_out)
{
/* krb5_dbe_get_mkvno() returns an error if we produce NULL, so return an
* empty node to make kadm5_get_principal() work. */
*mkeys_out = ealloc(sizeof(**mkeys_out));
return 0;
}
static krb5_error_code
test_decrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
const krb5_key_data *kd, krb5_keyblock *key_out,
krb5_keysalt *salt_out)
{
key_out->magic = KV5M_KEYBLOCK;
key_out->enctype = kd->key_data_type[0];
key_out->length = kd->key_data_length[0];
key_out->contents = ealloc(key_out->length);
memcpy(key_out->contents, kd->key_data_contents[0], key_out->length);
if (salt_out != NULL) {
salt_out->type = (kd->key_data_ver > 1) ? kd->key_data_type[1] :
KRB5_KDB_SALTTYPE_NORMAL;
salt_out->data = empty_data();
}
return 0;
}
static krb5_error_code
test_encrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
const krb5_keyblock *key, const krb5_keysalt *salt,
int kvno, krb5_key_data *kd_out)
{
memset(kd_out, 0, sizeof(*kd_out));
kd_out->key_data_ver = 2;
kd_out->key_data_kvno = kvno;
kd_out->key_data_type[0] = key->enctype;
kd_out->key_data_length[0] = key->length;
kd_out->key_data_contents[0] = ealloc(key->length);
memcpy(kd_out->key_data_contents[0], key->contents, key->length);
kd_out->key_data_type[1] = (salt != NULL) ? salt->type :
KRB5_KDB_SALTTYPE_NORMAL;
return 0;
}
typedef struct {
char *pac_princ;
struct {
char *proxy_target;
char *impersonator;
} deleg_info;
krb5_boolean not_delegated;
krb5_pac pac;
} pac_info;
static void
free_pac_info(krb5_context context, pac_info *info)
{
if (info == NULL)
return;
free(info->pac_princ);
free(info->deleg_info.proxy_target);
free(info->deleg_info.impersonator);
krb5_pac_free(context, info->pac);
free(info);
}
/*
* Create a PAC object with a fake logon-info blob. Instead of a real
* KERB_VALIDATION_INFO structure, store a byte indicating whether the
* USER_NOT_DELEGATED bit is set.
*/
static krb5_error_code
create_pac(krb5_context context, krb5_boolean not_delegated, krb5_pac *pac_out)
{
krb5_data data;
krb5_pac pac;
char nd;
nd = not_delegated ? 1 : 0;
data = make_data(&nd, 1);
check(krb5_pac_init(context, &pac));
check(krb5_pac_add_buffer(context, pac, KRB5_PAC_LOGON_INFO, &data));
*pac_out = pac;
return 0;
}
/* Create a fake PAC, setting the USER_NOT_DELEGATED bit if the client DB entry
* disallows forwardable tickets. */
static krb5_error_code
create_pac_db(krb5_context context, krb5_db_entry *client, krb5_pac *pac_out)
{
krb5_boolean not_delegated;
/* Use disallow_forwardable as delegation_not_allowed attribute */
not_delegated = (client->attributes & KRB5_KDB_DISALLOW_FORWARDABLE);
return create_pac(context, not_delegated, pac_out);
}
/* Locate the PAC in tgt_authdata and set *pac_out to its PAC object
* representation. Set it to NULL if no PAC is present. */
static void
parse_ticket_pac(krb5_context context, krb5_authdata **tgt_auth_data,
krb5_pac *pac_out)
{
krb5_authdata **authdata;
*pac_out = NULL;
check(krb5_find_authdata(context, tgt_auth_data, NULL,
KRB5_AUTHDATA_WIN2K_PAC, &authdata));
if (authdata == NULL)
return;
assert(authdata[1] == NULL);
check(krb5_pac_parse(context, authdata[0]->contents, authdata[0]->length,
pac_out));
krb5_free_authdata(context, authdata);
}
/* Verify the KDC signature against the local TGT key. tgt_key must be the
* decrypted first key data entry of tgt. */
static krb5_error_code
verify_kdc_signature(krb5_context context, krb5_pac pac,
krb5_keyblock *tgt_key, krb5_db_entry *tgt)
{
krb5_error_code ret;
krb5_key_data *kd;
krb5_keyblock old_key;
krb5_kvno kvno;
int tries;
ret = krb5_pac_verify(context, pac, 0, NULL, NULL, tgt_key);
if (ret != KRB5KRB_AP_ERR_BAD_INTEGRITY)
return ret;
kvno = tgt->key_data[0].key_data_kvno - 1;
/* There is no kvno in PAC signatures, so try two previous versions. */
for (tries = 2; tries > 0 && kvno > 0; tries--, kvno--) {
ret = krb5_dbe_find_enctype(context, tgt, -1, -1, kvno, &kd);
if (ret)
return KRB5KRB_AP_ERR_BAD_INTEGRITY;
ret = krb5_dbe_decrypt_key_data(context, NULL, kd, &old_key, NULL);
if (ret)
return ret;
ret = krb5_pac_verify(context, pac, 0, NULL, NULL, &old_key);
krb5_free_keyblock_contents(context, &old_key);
if (!ret)
return 0;
/* Try the next lower kvno on the next iteration. */
kvno = kd->key_data_kvno - 1;
}
return KRB5KRB_AP_ERR_BAD_INTEGRITY;
}
static krb5_error_code
verify_ticket_pac(krb5_context context, krb5_pac pac, unsigned int flags,
krb5_const_principal client_princ, krb5_boolean check_realm,
krb5_keyblock *server_key, krb5_keyblock *local_tgt_key,
krb5_db_entry *local_tgt, krb5_timestamp authtime)
{
check(krb5_pac_verify_ext(context, pac, authtime, client_princ, server_key,
NULL, check_realm));
if (flags & KRB5_KDB_FLAG_CROSS_REALM)
return 0;
return verify_kdc_signature(context, pac, local_tgt_key, local_tgt);
}
static void
get_pac_info(krb5_context context, krb5_authdata **in_authdata,
pac_info **info_out)
{
krb5_error_code ret;
krb5_pac pac = NULL;
krb5_data data;
char *sep = NULL;
pac_info *info;
*info_out = NULL;
parse_ticket_pac(context, in_authdata, &pac);
if (pac == NULL)
return;
info = ealloc(sizeof(*info));
/* Read the fake logon-info buffer from the PAC and set not_delegated
* according to the byte value. */
check(krb5_pac_get_client_info(context, pac, NULL, &info->pac_princ));
check(krb5_pac_get_buffer(context, pac, KRB5_PAC_LOGON_INFO, &data));
assert(data.length == 1);
info->not_delegated = *data.data;
krb5_free_data_contents(context, &data);
ret = krb5_pac_get_buffer(context, pac, KRB5_PAC_DELEGATION_INFO, &data);
if (ret && ret != ENOENT)
abort();
if (!ret) {
sep = memchr(data.data, ':', data.length);
assert(sep != NULL);
info->deleg_info.proxy_target = k5memdup0(data.data, sep - data.data,
&ret);
check(ret);
info->deleg_info.impersonator = k5memdup0(sep + 1, data.length - 1 -
(sep - data.data), &ret);
check(ret);
krb5_free_data_contents(context, &data);
}
info->pac = pac;
*info_out = info;
}
/* Add a fake delegation-info buffer to pac containing the proxy target and
* impersonator from info. */
static void
add_delegation_info(krb5_context context, krb5_pac pac, pac_info *info)
{
krb5_data data;
char *str;
if (info->deleg_info.proxy_target == NULL)
return;
if (asprintf(&str, "%s:%s", info->deleg_info.proxy_target,
info->deleg_info.impersonator) < 0)
abort();
data = string2data(str);
check(krb5_pac_add_buffer(context, pac, KRB5_PAC_DELEGATION_INFO, &data));
free(str);
}
/* Set *out to an AD-IF-RELEVANT authdata element containing a PAC authdata
* element with contents pac_data. */
static void
encode_pac_ad(krb5_context context, krb5_data *pac_data, krb5_authdata **out)
{
krb5_authdata pac_ad, *list[2], **ifrel;
pac_ad.magic = KV5M_AUTHDATA;
pac_ad.ad_type = KRB5_AUTHDATA_WIN2K_PAC;
pac_ad.contents = (krb5_octet *)pac_data->data;;
pac_ad.length = pac_data->length;
list[0] = &pac_ad;
list[1] = NULL;
check(krb5_encode_authdata_container(context, KRB5_AUTHDATA_IF_RELEVANT,
list, &ifrel));
assert(ifrel[1] == NULL);
*out = ifrel[0];
free(ifrel);
}
/* Parse a PAC client-info string into a principal name. If xrealm_s4u is
* true, expect a realm in the string. */
static krb5_error_code
parse_pac_princ(krb5_context context, krb5_boolean xrealm_s4u, char *pac_princ,
krb5_principal *client_out)
{
int n_atsigns = 0, flags = 0;
char *p = pac_princ;
while (*p++) {
if (*p == '@')
n_atsigns++;
}
if (xrealm_s4u) {
flags |= KRB5_PRINCIPAL_PARSE_REQUIRE_REALM;
n_atsigns--;
} else {
flags |= KRB5_PRINCIPAL_PARSE_NO_REALM;
}
assert(n_atsigns == 0 || n_atsigns == 1);
if (n_atsigns == 1)
flags |= KRB5_PRINCIPAL_PARSE_ENTERPRISE;
check(krb5_parse_name_flags(context, pac_princ, flags, client_out));
(*client_out)->type = KRB5_NT_MS_PRINCIPAL;
return 0;
}
/* Set *ad_out to a fake PAC for testing, or to NULL if it doesn't make sense
* to generate a PAC for the request. */
static void
generate_pac(krb5_context context, unsigned int flags,
krb5_const_principal client_princ,
krb5_const_principal server_princ, krb5_db_entry *client,
krb5_db_entry *header_server, krb5_db_entry *local_tgt,
krb5_keyblock *server_key, krb5_keyblock *header_key,
krb5_keyblock *local_tgt_key, krb5_timestamp authtime,
pac_info *info, krb5_authdata **ad_out)
{
krb5_boolean sign_realm, check_realm;
krb5_data pac_data;
krb5_pac pac = NULL;
krb5_principal pac_princ = NULL;
*ad_out = NULL;
check_realm = ((flags & KRB5_KDB_FLAGS_S4U) &&
(flags & KRB5_KDB_FLAG_CROSS_REALM));
sign_realm = ((flags & KRB5_KDB_FLAGS_S4U) &&
(flags & KRB5_KDB_FLAG_ISSUING_REFERRAL));
if (client != NULL &&
((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) ||
(flags & KRB5_KDB_FLAG_PROTOCOL_TRANSITION))) {
/* For AS or local-realm S4U2Self, generate an initial PAC. */
check(create_pac_db(context, client, &pac));
} else if (info == NULL) {
/* If there is no input PAC, do not generate one. */
assert((flags & KRB5_KDB_FLAGS_S4U) == 0);
return;
} else {
if (IS_TGS_PRINC(server_princ) &&
info->deleg_info.proxy_target != NULL) {
/* RBCD transitive trust. */
assert(flags & KRB5_KDB_FLAG_CROSS_REALM);
assert(!(flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION));
check(parse_pac_princ(context, TRUE, info->pac_princ, &pac_princ));
client_princ = pac_princ;
check_realm = TRUE;
sign_realm = TRUE;
} else if ((flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) &&
!(flags & KRB5_KDB_FLAG_CROSS_REALM)) {
/*
* Initial RBCD and old constrained delegation requests to
* impersonator realm; create delegation info blob. We cannot
* assume that proxy_target is NULL as the evidence ticket could
* have been acquired via constrained delegation.
*/
free(info->deleg_info.proxy_target);
check(krb5_unparse_name_flags(context, server_princ,
KRB5_PRINCIPAL_UNPARSE_NO_REALM,
&info->deleg_info.proxy_target));
/* This is supposed to be a list of impersonators, but we currently
* only deal with one. */
free(info->deleg_info.impersonator);
check(krb5_unparse_name(context, header_server->princ,
&info->deleg_info.impersonator));
} else if (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) {
/* Last cross realm RBCD request to proxy realm. */
assert(info->deleg_info.proxy_target != NULL);
}
/* We have already verified the PAC in get_authdata_info, but we should
* be able to verify the signatures here as well. */
check(verify_ticket_pac(context, info->pac, flags, client_princ,
check_realm, header_key, local_tgt_key,
local_tgt, authtime));
/* Create a new pac as we may be altering pac principal's realm */
check(create_pac(context, info->not_delegated, &pac));
add_delegation_info(context, pac, info);
}
check(krb5_pac_sign_ext(context, pac, authtime, client_princ, server_key,
local_tgt_key, sign_realm, &pac_data));
krb5_pac_free(context, pac);
krb5_free_principal(context, pac_princ);
encode_pac_ad(context, &pac_data, ad_out);
krb5_free_data_contents(context, &pac_data);
}
static krb5_error_code
test_sign_authdata(krb5_context context, unsigned int flags,
krb5_const_principal client_princ,
krb5_const_principal server_princ, krb5_db_entry *client,
krb5_db_entry *server, krb5_db_entry *header_server,
krb5_db_entry *local_tgt, krb5_keyblock *client_key,
krb5_keyblock *server_key, krb5_keyblock *header_key,
krb5_keyblock *local_tgt_key, krb5_keyblock *session_key,
krb5_timestamp authtime, krb5_authdata **tgt_auth_data,
void *ad_info, krb5_data ***auth_indicators,
krb5_authdata ***signed_auth_data)
{
krb5_authdata *pac_ad = NULL, *test_ad = NULL, **list;
krb5_data **inds, d;
int i, val;
/* Possibly create a PAC authdata element. */
generate_pac(context, flags, client_princ, server_princ, client,
header_server, local_tgt, server_key, header_key,
local_tgt_key, authtime, ad_info, &pac_ad);
/* Always create a TEST_AD_TYPE element. */
test_ad = ealloc(sizeof(*test_ad));
test_ad->magic = KV5M_AUTHDATA;
test_ad->ad_type = TEST_AD_TYPE;
test_ad->contents = (uint8_t *)estrdup("db-authdata-test");
test_ad->length = strlen((char *)test_ad->contents);
/* Assemble the authdata into a one-element or two-element list.
* The PAC must be the first element. */
list = ealloc(3 * sizeof(*list));
list[0] = (pac_ad != NULL) ? pac_ad : test_ad;
list[1] = (pac_ad != NULL) ? test_ad : NULL;
list[2] = NULL;
*signed_auth_data = list;
/* If we see an auth indicator "dbincrX", replace the whole indicator list
* with "dbincr{X+1}". */
inds = *auth_indicators;
for (i = 0; inds != NULL && inds[i] != NULL; i++) {
if (inds[i]->length == 7 && memcmp(inds[i]->data, "dbincr", 6) == 0) {
val = inds[i]->data[6];
k5_free_data_ptr_list(inds);
inds = ealloc(2 * sizeof(*inds));
d = string2data("dbincr0");
check(krb5_copy_data(context, &d, &inds[0]));
inds[0]->data[6] = val + 1;
inds[1] = NULL;
*auth_indicators = inds;
break;
}
}
return 0;
}
static krb5_boolean
match_in_table(krb5_context context, const char *table, const char *sprinc,
const char *tprinc)
{
testhandle h = context->dal_handle->db_context;
krb5_error_code ret;
char **values, **v;
krb5_boolean found = FALSE;
set_names(h, table, sprinc, NULL);
ret = profile_get_values(h->profile, h->names, &values);
assert(ret == 0 || ret == PROF_NO_RELATION);
if (ret)
return FALSE;
for (v = values; *v != NULL; v++) {
if (strcmp(*v, tprinc) == 0) {
found = TRUE;
break;
}
}
profile_free_list(values);
return found;
}
static krb5_error_code
test_check_allowed_to_delegate(krb5_context context,
krb5_const_principal client,
const krb5_db_entry *server,
krb5_const_principal proxy)
{
char *sprinc, *tprinc;
krb5_boolean found = FALSE;
check(krb5_unparse_name_flags(context, server->princ,
KRB5_PRINCIPAL_UNPARSE_NO_REALM, &sprinc));
check(krb5_unparse_name_flags(context, proxy,
KRB5_PRINCIPAL_UNPARSE_NO_REALM, &tprinc));
found = match_in_table(context, "delegation", sprinc, tprinc);
krb5_free_unparsed_name(context, sprinc);
krb5_free_unparsed_name(context, tprinc);
return found ? 0 : KRB5KDC_ERR_POLICY;
}
static krb5_error_code
test_allowed_to_delegate_from(krb5_context context,
krb5_const_principal client,
krb5_const_principal server,
void *server_ad_info, const krb5_db_entry *proxy)
{
char *sprinc, *tprinc;
pac_info *info = (pac_info *)server_ad_info;
krb5_boolean found = FALSE;
check(krb5_unparse_name(context, proxy->princ, &sprinc));
check(krb5_unparse_name(context, server, &tprinc));
assert(strncmp(info->pac_princ, tprinc, strlen(info->pac_princ)) == 0);
found = match_in_table(context, "rbcd", sprinc, tprinc);
krb5_free_unparsed_name(context, sprinc);
krb5_free_unparsed_name(context, tprinc);
return found ? 0 : KRB5KDC_ERR_POLICY;
}
static krb5_error_code
test_get_authdata_info(krb5_context context, unsigned int flags,
krb5_authdata **in_authdata,
krb5_const_principal client_princ,
krb5_const_principal server_princ,
krb5_keyblock *server_key, krb5_keyblock *krbtgt_key,
krb5_db_entry *krbtgt, krb5_timestamp authtime,
void **ad_info_out, krb5_principal *client_out)
{
pac_info *info = NULL;
krb5_boolean rbcd_transitive, xrealm_s4u;
krb5_principal pac_princ = NULL;
char *proxy_name = NULL, *impersonator_name = NULL;
get_pac_info(context, in_authdata, &info);
if (info == NULL)
return 0;
/* Transitive RBCD requests are not flagged as constrained delegation */
if (info->not_delegated &&
(info->deleg_info.proxy_target ||
(flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION))) {
free_pac_info(context, info);
return KRB5KDC_ERR_BADOPTION;
}
rbcd_transitive = IS_TGS_PRINC(server_princ) &&
(flags & KRB5_KDB_FLAG_CROSS_REALM) && info->deleg_info.proxy_target &&
!(flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION);
xrealm_s4u = rbcd_transitive || ((flags & KRB5_KDB_FLAG_CROSS_REALM) &&
(flags & KRB5_KDB_FLAGS_S4U));
check(parse_pac_princ(context, xrealm_s4u, info->pac_princ, &pac_princ));
/* Cross-realm and transitive trust RBCD requests */
if (rbcd_transitive || ((flags & KRB5_KDB_FLAG_CROSS_REALM) &&
(flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION))) {
assert(info->deleg_info.proxy_target != NULL);
assert(info->deleg_info.impersonator != NULL);
/* We must be able to find the impersonator in the delegation info. */
assert(!krb5_principal_compare(context, client_princ, pac_princ));
check(krb5_unparse_name(context, client_princ, &impersonator_name));
assert(strcmp(info->deleg_info.impersonator, impersonator_name) == 0);
krb5_free_unparsed_name(context, impersonator_name);
client_princ = pac_princ;
/* In the non-transitive case we can match the proxy too. */
if (!rbcd_transitive) {
check(krb5_unparse_name_flags(context, server_princ,
KRB5_PRINCIPAL_UNPARSE_NO_REALM,
&proxy_name));
assert(info->deleg_info.proxy_target != NULL);
assert(strcmp(info->deleg_info.proxy_target, proxy_name) == 0);
krb5_free_unparsed_name(context, proxy_name);
}
}
check(verify_ticket_pac(context, info->pac, flags, client_princ,
xrealm_s4u, server_key, krbtgt_key, krbtgt,
authtime));
*ad_info_out = info;
if (client_out != NULL)
*client_out = pac_princ;
else
krb5_free_principal(context, pac_princ);
return 0;
}
static void
test_free_authdata_info(krb5_context context, void *ad_info)
{
pac_info *info = (pac_info *)ad_info;
free_pac_info(context, info);
}
kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_test, kdb_function_table) = {
KRB5_KDB_DAL_MAJOR_VERSION, /* major version number */
0, /* minor version number */
test_init,
test_cleanup,
test_open,
test_close,
NULL, /* create */
NULL, /* destroy */
NULL, /* get_age */
NULL, /* lock */
NULL, /* unlock */
test_get_principal,
NULL, /* put_principal */
NULL, /* delete_principal */
NULL, /* rename_principal */
NULL, /* iterate */
NULL, /* create_policy */
NULL, /* get_policy */
NULL, /* put_policy */
NULL, /* iter_policy */
NULL, /* delete_policy */
test_fetch_master_key,
test_fetch_master_key_list,
NULL, /* store_master_key_list */
NULL, /* dbe_search_enctype */
NULL, /* change_pwd */
NULL, /* promote_db */
test_decrypt_key_data,
test_encrypt_key_data,
test_sign_authdata,
NULL, /* check_transited_realms */
NULL, /* check_policy_as */
NULL, /* check_policy_tgs */
NULL, /* audit_as_req */
NULL, /* refresh_config */
test_check_allowed_to_delegate,
NULL, /* free_principal_e_data */
test_get_s4u_x509_principal,
test_allowed_to_delegate_from,
test_get_authdata_info,
test_free_authdata_info
};