Implement forwarding of leaf TGTs to selected realms.

Refactor and enhance TGT forwarding to allow forwarding of leaf
(destination) TGTs for selected destination realms.

Enhance kinit(1) to renew non-origin realm tickets

Document delegate-destination-tgt

Use the newly implemented _krb5_mk_1cred().
This commit is contained in:
Viktor Dukhovni
2019-10-29 20:02:14 +00:00
committed by Nico Williams
parent d81118cc1f
commit 5bbe7c8dc6
6 changed files with 326 additions and 310 deletions

View File

@@ -115,10 +115,18 @@ like
.It Fl p , Fl Fl proxiable
Request tickets with the proxiable flag set.
.It Fl R , Fl Fl renew
Try to renew ticket.
Try to renew a ticket.
The ticket must have the
.Sq renewable
flag set, and must not be expired.
flag set, and must not be expired. If the
.Oo Fl S Ar principal Oc
option is specified, the ticket for the indicated service is renewed.
If no service is explicitly specified, an attempt is made to renew the
TGT for the client realm. If no TGT for the client realm is found in the
credential cache, an attempt is made to renew the TGT for the defaualt
realm (if that is found in the credential cache), or else the first
TGT found. This makes it easier for users to renew forwarded tickets
that are not issued by the origin realm.
.It Fl Fl renewable
The same as
.Fl Fl renewable-life ,

View File

@@ -61,6 +61,7 @@ int anonymous_flag = 0;
char *lifetime = NULL;
char *renew_life = NULL;
char *server_str = NULL;
static krb5_principal tgs_service;
char *cred_cache = NULL;
char *start_str = NULL;
static int switch_cache_flags = 1;
@@ -217,19 +218,125 @@ usage(int ret)
exit(ret);
}
static krb5_error_code
tgs_principal(krb5_context context,
krb5_ccache cache,
krb5_principal client,
krb5_const_realm tgs_realm,
krb5_principal *out_princ)
{
krb5_error_code ret;
krb5_principal tgs_princ;
krb5_creds creds;
krb5_creds *tick;
krb5_flags options;
ret = krb5_make_principal(context, &tgs_princ, tgs_realm,
KRB5_TGS_NAME, tgs_realm, NULL);
if (ret)
return ret;
/*
* Don't fail-over to a different realm just because a TGT expired
*/
options = KRB5_GC_CACHED | KRB5_GC_EXPIRED_OK;
memset(&creds, 0, sizeof(creds));
creds.client = client;
creds.server = tgs_princ;
ret = krb5_get_credentials(context, options, cache, &creds, &tick);
if (ret == 0) {
krb5_free_creds(context, tick);
*out_princ = tgs_princ;
} else {
krb5_free_principal(context, tgs_princ);
}
return ret;
}
/*
* Try TGS specified with '-S',
* then TGS of client realm,
* then if fallback is FALSE: fail,
* otherwise try TGS of default realm,
* and finally first TGT in ccache.
*/
static krb5_error_code
get_server(krb5_context context,
krb5_ccache cache,
krb5_principal client,
const char *server,
krb5_boolean fallback,
krb5_principal *princ)
{
krb5_error_code ret = 0;
krb5_const_realm realm;
if (server)
return krb5_parse_name(context, server, princ);
krb5_realm def_realm;
krb5_cc_cursor cursor;
krb5_creds creds;
const char *pcomp;
if (tgs_service)
goto done;
if (server) {
ret = krb5_parse_name(context, server, &tgs_service);
goto done;
}
/* Try the client realm first */
realm = krb5_principal_get_realm(context, client);
return krb5_make_principal(context, princ, realm,
KRB5_TGS_NAME, realm, NULL);
ret = tgs_principal(context, cache, client, realm, &tgs_service);
if (ret == 0 || ret != KRB5_CC_NOTFOUND)
goto done;
if (!fallback)
return ret;
/* Next try the default realm */
ret = krb5_get_default_realm(context, &def_realm);
if (ret)
return ret;
ret = tgs_principal(context, cache, client, def_realm, &tgs_service);
free(def_realm);
if (ret == 0 || ret != KRB5_CC_NOTFOUND)
goto done;
/* Finally try the first TGT with instance == realm in the cache */
ret = krb5_cc_start_seq_get(context, cache, &cursor);
if (ret)
return ret;
for (/**/; ret == 0; krb5_free_cred_contents (context, &creds)) {
ret = krb5_cc_next_cred(context, cache, &cursor, &creds);
if (ret)
break;
if (creds.server->name.name_string.len != 2)
continue;
pcomp = krb5_principal_get_comp_string(context, creds.server, 0);
if (strcmp(pcomp, KRB5_TGS_NAME) != 0)
continue;
realm = krb5_principal_get_realm(context, creds.server);
pcomp = krb5_principal_get_comp_string(context, creds.server, 1);
if (strcmp(realm, pcomp) != 0)
continue;
ret = krb5_copy_principal(context, creds.server, &tgs_service);
break;
}
if (ret == KRB5_CC_END) {
ret = KRB5_CC_NOTFOUND;
krb5_set_error_message(context, ret,
N_("Credential cache contains no TGTs", ""));
}
krb5_cc_end_seq_get(context, cache, &cursor);
done:
if (!ret)
ret = krb5_copy_principal(context, tgs_service, princ);
return ret;
}
static krb5_error_code
@@ -333,7 +440,7 @@ renew_validate(krb5_context context,
KRB5_ANON_MATCH_UNAUTHENTICATED))
ret = get_anon_pkinit_tgs_name(context, cache, &in.server);
else
ret = get_server(context, in.client, server, &in.server);
ret = get_server(context, cache, in.client, server, TRUE, &in.server);
if (ret) {
krb5_warn(context, ret, "get_server");
goto out;
@@ -855,7 +962,10 @@ ticket_lifetime(krb5_context context, krb5_ccache cache, krb5_principal client,
krb5_warn(context, ret, "krb5_cc_get_principal");
return 0;
}
ret = get_server(context, in_cred.client, server, &in_cred.server);
/* Determine TGS principal without fallback */
ret = get_server(context, cache, in_cred.client, server, FALSE,
&in_cred.server);
if (ret) {
krb5_free_principal(context, in_cred.client);
krb5_warn(context, ret, "get_server");

View File

@@ -314,47 +314,29 @@ do_delegation (krb5_context context,
krb5_auth_context ac,
krb5_ccache ccache,
krb5_creds *cred,
krb5_const_principal name,
krb5_const_principal server,
krb5_data *fwd_data,
uint32_t flagmask,
uint32_t *flags)
{
krb5_creds creds;
KDCOptions fwd_flags;
krb5_error_code kret;
krb5_principal client;
const char *host;
memset (&creds, 0, sizeof(creds));
krb5_data_zero (fwd_data);
kret = krb5_cc_get_principal(context, ccache, &creds.client);
kret = krb5_cc_get_principal(context, ccache, &client);
if (kret)
goto out;
kret = krb5_make_principal(context,
&creds.server,
creds.client->realm,
KRB5_TGS_NAME,
creds.client->realm,
NULL);
if (kret)
/* We can't generally enforce server.name_type == KRB5_NT_SRV_HST */
if (server->name.name_string.len < 2)
goto out;
host = krb5_principal_get_comp_string(context, server, 1);
creds.times.endtime = 0;
memset(&fwd_flags, 0, sizeof(fwd_flags));
fwd_flags.forwarded = 1;
fwd_flags.forwardable = 1;
if (name->name.name_string.len < 2)
goto out;
kret = krb5_get_forwarded_creds(context,
ac,
ccache,
KDCOptions2int(fwd_flags),
name->name.name_string.val[1],
&creds,
fwd_data);
#define FWDABLE 1
kret = krb5_fwd_tgt_creds(context, ac, host, client, server, ccache,
FWDABLE, fwd_data);
out:
if (kret)
@@ -362,10 +344,8 @@ do_delegation (krb5_context context,
else
*flags |= flagmask;
if (creds.client)
krb5_free_principal(context, creds.client);
if (creds.server)
krb5_free_principal(context, creds.server);
if (client)
krb5_free_principal(context, client);
}
/*

View File

@@ -1481,6 +1481,8 @@ krb5_make_addrport (krb5_context context,
size_t len = addr->address.length + 2 + 4 * 4;
u_char *p;
/* XXX Make this assume port == 0 -> port is absent */
*res = malloc (sizeof(**res));
if (*res == NULL)
return krb5_enomem(context);

View File

@@ -33,6 +33,14 @@
#include "krb5_locl.h"
static krb5_error_code set_tgs_creds(krb5_context, krb5_ccache,
krb5_const_principal,
krb5_const_principal, krb5_creds *);
static krb5_error_code get_cred(krb5_context, krb5_ccache, krb5_creds *,
krb5_flags, const char *, krb5_creds **);
static krb5_error_code get_addresses(krb5_context, krb5_ccache, krb5_creds *,
const char *, krb5_addresses *);
static krb5_error_code
add_addrs(krb5_context context,
krb5_addresses *addr,
@@ -81,10 +89,16 @@ fail:
}
/**
* Forward credentials for client to host hostname , making them
* Forward credentials for client to host hostname, making them
* forwardable if forwardable, and returning the blob of data to sent
* in out_data. If hostname == NULL, pick it from server.
*
* If the server's realm is configured for delegation of destination
* TGTs, forward a TGT for the server realm, rather than the client
* realm. This works better with destinations on the far side of a
* firewall. We also forward the destination TGT when the client
* TGT is not available (we may have just the destination TGT).
*
* @param context A kerberos 5 context.
* @param auth_context the auth context with the key to encrypt the out_data.
* @param hostname the host to forward the tickets too.
@@ -100,19 +114,18 @@ fail:
*/
KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
krb5_fwd_tgt_creds (krb5_context context,
krb5_auth_context auth_context,
const char *hostname,
krb5_principal client,
krb5_principal server,
krb5_ccache ccache,
int forwardable,
krb5_data *out_data)
krb5_fwd_tgt_creds(krb5_context context,
krb5_auth_context auth_context,
const char *hostname,
krb5_const_principal client,
krb5_const_principal server,
krb5_ccache ccache,
int forwardable,
krb5_data *out_data)
{
krb5_flags flags = 0;
krb5_creds creds;
krb5_error_code ret;
krb5_const_realm client_realm;
flags |= KDC_OPT_FORWARDED;
@@ -131,17 +144,11 @@ krb5_fwd_tgt_creds (krb5_context context,
hostname = host;
}
client_realm = krb5_principal_get_realm(context, client);
memset (&creds, 0, sizeof(creds));
creds.client = client;
ret = krb5_make_principal(context,
&creds.server,
client_realm,
KRB5_TGS_NAME,
client_realm,
NULL);
/*
* Fill-in the request creds, the server principal will be the TGS
* of either the client's or the server's realm.
*/
ret = set_tgs_creds(context, ccache, client, server, &creds);
if (ret)
return ret;
@@ -152,6 +159,8 @@ krb5_fwd_tgt_creds (krb5_context context,
hostname,
&creds,
out_data);
krb5_free_cred_contents(context, &creds);
return ret;
}
@@ -192,273 +201,167 @@ krb5_get_forwarded_creds (krb5_context context,
krb5_data *out_data)
{
krb5_error_code ret;
krb5_creds *out_creds;
krb5_addresses addrs, *paddrs;
KRB_CRED cred;
KrbCredInfo *krb_cred_info;
EncKrbCredPart enc_krb_cred_part;
size_t len;
unsigned char *buf;
size_t buf_size;
krb5_kdc_flags kdc_flags;
krb5_crypto crypto;
struct addrinfo *ai;
krb5_creds *ticket;
krb5_creds *creds;
/* Obtain the requested TGT */
ret = get_cred(context, ccache, in_creds, flags, hostname, &creds);
if (ret)
return ret;
/* Forward obtained creds */
ret = _krb5_mk_1cred(context, auth_context, creds, out_data, NULL);
krb5_free_creds(context, creds);
return ret;
}
/*
* Get a TGT for forwarding to hostname. If the client TGT is
* addressless, the forwarded ticket will also be addressless.
*
* If the TGT has any addresses, hostname will be used to determine
* the address to forward the ticket to. Thus, since this might use DNS,
* it's insecure and also may not capture all the addresses of the host.
* In general addressless tickets are more robust, be it at a small
* security penalty.
*
* @param context A kerberos 5 context.
* @param ccache The credential cache to use
* @param creds Creds with client and server principals
* @param flags The flags to control the resulting ticket flags
* @param hostname The hostname of server
* @param out_creds The resulting credential
*
* @return Return an error code or 0.
*/
static krb5_error_code
get_cred(krb5_context context,
krb5_ccache ccache,
krb5_creds *creds,
krb5_flags flags,
const char *hostname,
krb5_creds **out_creds)
{
krb5_error_code ret;
krb5_kdc_flags kdc_flags;
krb5_addresses addrs;
paddrs = NULL;
addrs.len = 0;
addrs.val = NULL;
ret = get_addresses(context, ccache, creds, hostname, &addrs);
if (ret)
return ret;
ret = krb5_get_credentials(context, 0, ccache, in_creds, &ticket);
if(ret == 0) {
if (ticket->addresses.len)
paddrs = &addrs;
krb5_free_creds (context, ticket);
} else {
krb5_boolean noaddr;
krb5_appdefault_boolean(context, NULL,
krb5_principal_get_realm(context,
in_creds->client),
"no-addresses", KRB5_ADDRESSLESS_DEFAULT,
&noaddr);
if (!noaddr)
paddrs = &addrs;
kdc_flags.b = int2KDCOptions(flags);
ret = krb5_get_kdc_cred(context, ccache, kdc_flags, &addrs, NULL,
creds, out_creds);
krb5_free_addresses(context, &addrs);
return ret;
}
static krb5_error_code
set_tgs_creds(krb5_context context,
krb5_ccache ccache,
krb5_const_principal client,
krb5_const_principal server,
krb5_creds *creds)
{
krb5_error_code ret;
krb5_const_realm client_realm;
krb5_const_realm server_realm;
krb5_boolean fwd_dest_tgt;
krb5_creds *client_tgt;
client_realm = krb5_principal_get_realm(context, client);
server_realm = krb5_principal_get_realm(context, server);
memset (creds, 0, sizeof(*creds));
ret = krb5_copy_principal(context, client, &creds->client);
if (ret)
return ret;
ret = krb5_make_principal(context, &creds->server, client_realm,
KRB5_TGS_NAME, client_realm, NULL);
if (ret) {
krb5_free_principal(context, creds->client);
return ret;
}
/*
* If tickets have addresses, get the address of the remote host.
* Optionally delegate a TGT for the server's realm, rather than
* the client's. Do this also when we don't have a client realm TGT.
*
* XXX: Note, when we have a start-realm, and delegate-destination-tgt
* is not set, we must use the start-realm.
*/
krb5_appdefault_boolean(context, NULL, server_realm,
"delegate-destination-tgt", FALSE, &fwd_dest_tgt);
if (paddrs != NULL) {
ret = getaddrinfo (hostname, NULL, NULL, &ai);
if (ret) {
krb5_error_code ret2 = krb5_eai_to_heim_errno(ret, errno);
krb5_set_error_message(context, ret2,
N_("resolving host %s failed: %s",
"hostname, error"),
hostname, gai_strerror(ret));
return ret2;
}
ret = add_addrs (context, &addrs, ai);
freeaddrinfo (ai);
if (ret)
if (!fwd_dest_tgt) {
ret = krb5_get_credentials(context, KRB5_GC_CACHED, ccache, creds,
&client_tgt);
if (ret == 0) {
krb5_free_creds(context, client_tgt);
return ret;
}
kdc_flags.b = int2KDCOptions(flags);
ret = krb5_get_kdc_cred (context,
ccache,
kdc_flags,
paddrs,
NULL,
in_creds,
&out_creds);
krb5_free_addresses (context, &addrs);
if (ret)
return ret;
memset (&cred, 0, sizeof(cred));
cred.pvno = 5;
cred.msg_type = krb_cred;
ALLOC_SEQ(&cred.tickets, 1);
if (cred.tickets.val == NULL) {
ret = krb5_enomem(context);
goto out2;
}
ret = decode_Ticket(out_creds->ticket.data,
out_creds->ticket.length,
cred.tickets.val, &len);
if (ret)
goto out3;
memset (&enc_krb_cred_part, 0, sizeof(enc_krb_cred_part));
ALLOC_SEQ(&enc_krb_cred_part.ticket_info, 1);
if (enc_krb_cred_part.ticket_info.val == NULL) {
ret = krb5_enomem(context);
goto out4;
}
if (auth_context->flags & KRB5_AUTH_CONTEXT_DO_TIME) {
krb5_timestamp sec;
int32_t usec;
krb5_us_timeofday (context, &sec, &usec);
ALLOC(enc_krb_cred_part.timestamp, 1);
if (enc_krb_cred_part.timestamp == NULL) {
ret = krb5_enomem(context);
goto out4;
}
*enc_krb_cred_part.timestamp = sec;
ALLOC(enc_krb_cred_part.usec, 1);
if (enc_krb_cred_part.usec == NULL) {
ret = krb5_enomem(context);
goto out4;
}
*enc_krb_cred_part.usec = usec;
} else {
enc_krb_cred_part.timestamp = NULL;
enc_krb_cred_part.usec = NULL;
}
if (auth_context->local_address && auth_context->local_port && paddrs) {
ret = krb5_make_addrport (context,
&enc_krb_cred_part.s_address,
auth_context->local_address,
auth_context->local_port);
if (ret)
goto out4;
}
if (auth_context->remote_address) {
if (auth_context->remote_port) {
krb5_boolean noaddr;
krb5_const_realm srealm;
srealm = krb5_principal_get_realm(context, out_creds->server);
/* Is this correct, and should we use the paddrs == NULL
trick here as well? Having an address-less ticket may
indicate that we don't know our own global address, but
it does not necessary mean that we don't know the
server's. */
krb5_appdefault_boolean(context, NULL, srealm, "no-addresses",
FALSE, &noaddr);
if (!noaddr) {
ret = krb5_make_addrport (context,
&enc_krb_cred_part.r_address,
auth_context->remote_address,
auth_context->remote_port);
if (ret)
goto out4;
}
} else {
ALLOC(enc_krb_cred_part.r_address, 1);
if (enc_krb_cred_part.r_address == NULL) {
ret = krb5_enomem(context);
goto out4;
}
ret = krb5_copy_address (context, auth_context->remote_address,
enc_krb_cred_part.r_address);
if (ret)
goto out4;
}
}
/* fill ticket_info.val[0] */
enc_krb_cred_part.ticket_info.len = 1;
krb_cred_info = enc_krb_cred_part.ticket_info.val;
ret = copy_EncryptionKey (&out_creds->session, &krb_cred_info->key);
if (ret)
goto out4;
ALLOC(krb_cred_info->prealm, 1);
ret = copy_Realm (&out_creds->client->realm, krb_cred_info->prealm);
if (ret)
goto out4;
ALLOC(krb_cred_info->pname, 1);
ret = copy_PrincipalName(&out_creds->client->name, krb_cred_info->pname);
if (ret)
goto out4;
ALLOC(krb_cred_info->flags, 1);
*krb_cred_info->flags = out_creds->flags.b;
ALLOC(krb_cred_info->authtime, 1);
*krb_cred_info->authtime = out_creds->times.authtime;
ALLOC(krb_cred_info->starttime, 1);
*krb_cred_info->starttime = out_creds->times.starttime;
ALLOC(krb_cred_info->endtime, 1);
*krb_cred_info->endtime = out_creds->times.endtime;
ALLOC(krb_cred_info->renew_till, 1);
*krb_cred_info->renew_till = out_creds->times.renew_till;
ALLOC(krb_cred_info->srealm, 1);
ret = copy_Realm (&out_creds->server->realm, krb_cred_info->srealm);
if (ret)
goto out4;
ALLOC(krb_cred_info->sname, 1);
ret = copy_PrincipalName (&out_creds->server->name, krb_cred_info->sname);
if (ret)
goto out4;
ALLOC(krb_cred_info->caddr, 1);
ret = copy_HostAddresses (&out_creds->addresses, krb_cred_info->caddr);
if (ret)
goto out4;
krb5_free_creds (context, out_creds);
/* encode EncKrbCredPart */
ASN1_MALLOC_ENCODE(EncKrbCredPart, buf, buf_size,
&enc_krb_cred_part, &len, ret);
free_EncKrbCredPart (&enc_krb_cred_part);
if (ret) {
free_KRB_CRED(&cred);
return ret;
}
if(buf_size != len)
krb5_abortx(context, "internal error in ASN.1 encoder");
/**
* Some older of the MIT gssapi library used clear-text tickets
* (warped inside AP-REQ encryption), use the krb5_auth_context
* flag KRB5_AUTH_CONTEXT_CLEAR_FORWARDED_CRED to support those
* tickets. The session key is used otherwise to encrypt the
* forwarded ticket.
/*
* Client TGT inapplicable or unavailable
*/
krb5_free_principal(context, creds->server);
creds->server = 0;
return krb5_make_principal(context, &creds->server, server_realm,
KRB5_TGS_NAME, server_realm, NULL);
}
if (auth_context->flags & KRB5_AUTH_CONTEXT_CLEAR_FORWARDED_CRED) {
cred.enc_part.etype = KRB5_ENCTYPE_NULL;
cred.enc_part.kvno = NULL;
cred.enc_part.cipher.data = buf;
cred.enc_part.cipher.length = buf_size;
/*
* Obtain address list for hostname if server realm policy is not addressless.
*/
static krb5_error_code
get_addresses(krb5_context context,
krb5_ccache ccache,
krb5_creds *creds,
const char *hostname,
krb5_addresses *addrs)
{
krb5_error_code ret;
krb5_creds *ticket;
krb5_const_realm realm;
krb5_boolean noaddr;
struct addrinfo *ai;
int eai;
if (hostname == 0)
return 0;
ret = krb5_get_credentials(context, 0, ccache, creds, &ticket);
if (ret == 0) {
noaddr = (ticket->addresses.len == 0) ? TRUE : FALSE;
krb5_free_creds(context, ticket);
} else {
/*
* Here older versions then 0.7.2 of Heimdal used the local or
* remote subkey. That is wrong, the session key should be
* used. Heimdal 0.7.2 and newer have code to try both in the
* receiving end.
*/
ret = krb5_crypto_init(context, auth_context->keyblock, 0, &crypto);
if (ret) {
free(buf);
free_KRB_CRED(&cred);
return ret;
}
ret = krb5_encrypt_EncryptedData (context,
crypto,
KRB5_KU_KRB_CRED,
buf,
len,
0,
&cred.enc_part);
free(buf);
krb5_crypto_destroy(context, crypto);
if (ret) {
free_KRB_CRED(&cred);
return ret;
}
realm = krb5_principal_get_realm(context, creds->server);
krb5_appdefault_boolean(context, NULL, realm, "no-addresses",
KRB5_ADDRESSLESS_DEFAULT, &noaddr);
}
ASN1_MALLOC_ENCODE(KRB_CRED, buf, buf_size, &cred, &len, ret);
free_KRB_CRED (&cred);
if (ret)
if (noaddr)
return 0;
/* Need addresses, get the address of the remote host. */
eai = getaddrinfo (hostname, NULL, NULL, &ai);
if (eai) {
ret = krb5_eai_to_heim_errno(eai, errno);
krb5_set_error_message(context, ret,
N_("resolving host %s failed: %s",
"hostname, error"),
hostname, gai_strerror(eai));
return ret;
if(buf_size != len)
krb5_abortx(context, "internal error in ASN.1 encoder");
out_data->length = len;
out_data->data = buf;
return 0;
out4:
free_EncKrbCredPart(&enc_krb_cred_part);
out3:
free_KRB_CRED(&cred);
out2:
krb5_free_creds (context, out_creds);
}
ret = add_addrs(context, addrs, ai);
freeaddrinfo(ai);
return ret;
}

View File

@@ -162,6 +162,19 @@ If a principal argument is specified, it is used as an explicit realm name for
anonymous pkinit even without an
.Li @
prefix.
.It Li delegate-destination-tgt = Va boolean
When forwarding credentials to a remote host, forward a TGT for the
realm of the destination host rather than a TGT for the user's realm.
This is useful when hosts in the remote realm should not or cannot
(e.g. firewalled from user realm's KDC) obtain tickets for services
in the user's realm. When the user's realm and the host's realm are
the same, this parameter has no effect. The setting can be applied
to a single realm as follows:
.Bd -literal -offset indent
EXAMPLE.COM = {
delegate-destination-tgt = true
}
.Ed
.El
.It Li [libdefaults]
.Bl -tag -width "xxx" -offset indent