diff --git a/Cache-S4U2Proxy-requests-by-second-ticket.patch b/Cache-S4U2Proxy-requests-by-second-ticket.patch new file mode 100644 index 0000000..01532bf --- /dev/null +++ b/Cache-S4U2Proxy-requests-by-second-ticket.patch @@ -0,0 +1,327 @@ +From 158ce9222351216507da39d7bb4233469603e647 Mon Sep 17 00:00:00 2001 +From: Isaac Boukris +Date: Tue, 21 Jul 2020 00:40:06 +0200 +Subject: [PATCH] Cache S4U2Proxy requests by second ticket + +krb5_get_credentials() does not know the client principal for an +S4U2Proxy request until the end, because it is in the encrypted part +of the evidence ticket. However, we can check the cache by second +ticket, since all S4U2Proxy requests in a cache will generally be made +with the same evidence ticket. + +In the ccache types, allow mcreds->client and mcreds->server to be +NULL (as Heimdal does) to ignore them for the purpose of matching. In +krb5int_construct_matching_creds(), set mcreds->client to NULL for +S4U2Proxy requests. Add a cache check to +k5_get_proxy_cred_from_kdc(), and remove the cache check from +krb5_get_credentials_for_proxy() and the krb5 mech's +get_credentials(). + +In get_proxy_cred_from_kdc(), fix a bug where cross-realm S4U2Proxy +would cache the evidence ticket used in the final request, rather than +the original evidence ticket. + +[ghudson@mit.edu: debugged cache check and cross-realm caching; +switched from new flag to null matching cred principals; wrote commit +message] + +ticket: 8931 (new) +(cherry picked from commit 148b317e1eb5df28dad96679cb4b8a07c62d4786) +--- + src/lib/gssapi/krb5/init_sec_context.c | 61 ++++++++++++-------------- + src/lib/krb5/ccache/cc_retr.c | 13 +++--- + src/lib/krb5/ccache/ccapi/stdcc_util.c | 30 ++++++------- + src/lib/krb5/ccache/ccfns.c | 3 +- + src/lib/krb5/krb/get_creds.c | 5 +++ + src/lib/krb5/krb/s4u_creds.c | 58 ++++++++++++------------ + src/tests/s4u2proxy.c | 3 ++ + 7 files changed, 88 insertions(+), 85 deletions(-) + +diff --git a/src/lib/gssapi/krb5/init_sec_context.c b/src/lib/gssapi/krb5/init_sec_context.c +index 3f77157c9..823656037 100644 +--- a/src/lib/gssapi/krb5/init_sec_context.c ++++ b/src/lib/gssapi/krb5/init_sec_context.c +@@ -165,44 +165,37 @@ static krb5_error_code get_credentials(context, cred, server, now, + goto cleanup; + } + +- /* +- * For IAKERB or constrained delegation, only check the cache in this step. +- * For IAKERB we will ask the server to make any necessary TGS requests; +- * for constrained delegation we will adjust in_creds and make an S4U2Proxy +- * request below if the cache lookup fails. +- */ +- if (cred->impersonator != NULL || cred->iakerb_mech) ++ /* Try constrained delegation if we have proxy credentials. */ ++ if (cred->impersonator != NULL) { ++ /* If we are trying to get a ticket to ourselves, we should use the ++ * the evidence ticket directly from cache. */ ++ if (krb5_principal_compare(context, cred->impersonator, ++ server->princ)) { ++ flags |= KRB5_GC_CACHED; ++ } else { ++ memset(&mcreds, 0, sizeof(mcreds)); ++ mcreds.magic = KV5M_CREDS; ++ mcreds.server = cred->impersonator; ++ mcreds.client = cred->name->princ; ++ code = krb5_cc_retrieve_cred(context, cred->ccache, ++ KRB5_TC_MATCH_AUTHDATA, &mcreds, ++ &evidence_creds); ++ if (code) ++ goto cleanup; ++ ++ in_creds.client = cred->impersonator; ++ in_creds.second_ticket = evidence_creds.ticket; ++ flags = KRB5_GC_CANONICALIZE | KRB5_GC_CONSTRAINED_DELEGATION; ++ } ++ } ++ ++ /* For IAKERB, only check the cache in this step. We will ask the server ++ * to make any necessary TGS requests. */ ++ if (cred->iakerb_mech) + flags |= KRB5_GC_CACHED; + + code = krb5_get_credentials(context, flags, cred->ccache, + &in_creds, &result_creds); +- +- /* +- * Try constrained delegation if we have proxy credentials, unless +- * we are trying to get a ticket to ourselves (in which case we could +- * just use the evidence ticket directly from cache). +- */ +- if (code == KRB5_CC_NOTFOUND && cred->impersonator != NULL && +- !cred->iakerb_mech && +- !krb5_principal_compare(context, cred->impersonator, server->princ)) { +- +- memset(&mcreds, 0, sizeof(mcreds)); +- mcreds.magic = KV5M_CREDS; +- mcreds.server = cred->impersonator; +- mcreds.client = cred->name->princ; +- code = krb5_cc_retrieve_cred(context, cred->ccache, +- KRB5_TC_MATCH_AUTHDATA, &mcreds, +- &evidence_creds); +- if (code) +- goto cleanup; +- +- in_creds.client = cred->impersonator; +- in_creds.second_ticket = evidence_creds.ticket; +- flags = KRB5_GC_CANONICALIZE | KRB5_GC_CONSTRAINED_DELEGATION; +- code = krb5_get_credentials(context, flags, cred->ccache, +- &in_creds, &result_creds); +- } +- + if (code) + goto cleanup; + +diff --git a/src/lib/krb5/ccache/cc_retr.c b/src/lib/krb5/ccache/cc_retr.c +index 2c50c9cce..4328b7d6f 100644 +--- a/src/lib/krb5/ccache/cc_retr.c ++++ b/src/lib/krb5/ccache/cc_retr.c +@@ -58,15 +58,14 @@ static krb5_boolean + princs_match(krb5_context context, krb5_flags whichfields, + const krb5_creds *mcreds, const krb5_creds *creds) + { +- krb5_principal_data princ; +- +- if (!krb5_principal_compare(context, mcreds->client, creds->client)) ++ if (mcreds->client != NULL && ++ !krb5_principal_compare(context, mcreds->client, creds->client)) + return FALSE; ++ if (mcreds->server == NULL) ++ return TRUE; + if (whichfields & KRB5_TC_MATCH_SRV_NAMEONLY) { +- /* Ignore the server realm. */ +- princ = *mcreds->server; +- princ.realm = creds->server->realm; +- return krb5_principal_compare(context, &princ, creds->server); ++ return krb5_principal_compare_any_realm(context, mcreds->server, ++ creds->server); + } else { + return krb5_principal_compare(context, mcreds->server, creds->server); + } +diff --git a/src/lib/krb5/ccache/ccapi/stdcc_util.c b/src/lib/krb5/ccache/ccapi/stdcc_util.c +index 1f2a3865c..58aa81165 100644 +--- a/src/lib/krb5/ccache/ccapi/stdcc_util.c ++++ b/src/lib/krb5/ccache/ccapi/stdcc_util.c +@@ -945,8 +945,13 @@ standard_fields_match(context, mcreds, creds) + krb5_context context; + const krb5_creds *mcreds, *creds; + { +- return (krb5_principal_compare(context, mcreds->client,creds->client) && +- krb5_principal_compare(context, mcreds->server,creds->server)); ++ if (mcreds->client != NULL && ++ !krb5_principal_compare(context, mcreds->client, creds->client)) ++ return FALSE; ++ if (mcreds->server != NULL && ++ !krb5_principal_compare(context, mcreds->server,creds->server)) ++ return FALSE; ++ return TRUE; + } + + /* only match the server name portion, not the server realm portion */ +@@ -956,19 +961,14 @@ srvname_match(context, mcreds, creds) + krb5_context context; + const krb5_creds *mcreds, *creds; + { +- krb5_boolean retval; +- krb5_principal_data p1, p2; +- +- retval = krb5_principal_compare(context, mcreds->client,creds->client); +- if (retval != TRUE) +- return retval; +- /* +- * Hack to ignore the server realm for the purposes of the compare. +- */ +- p1 = *mcreds->server; +- p2 = *creds->server; +- p1.realm = p2.realm; +- return krb5_principal_compare(context, &p1, &p2); ++ if (mcreds->client != NULL && ++ !krb5_principal_compare(context, mcreds->client, creds->client)) ++ return FALSE; ++ if (mcreds->server != NULL && ++ !krb5_principal_compare_any_realm(context, mcreds->server, ++ creds->server)) ++ return FALSE; ++ return TRUE; + } + + +diff --git a/src/lib/krb5/ccache/ccfns.c b/src/lib/krb5/ccache/ccfns.c +index 62a6983d8..59982b752 100644 +--- a/src/lib/krb5/ccache/ccfns.c ++++ b/src/lib/krb5/ccache/ccfns.c +@@ -96,7 +96,8 @@ krb5_cc_retrieve_cred(krb5_context context, krb5_ccache cache, + TRACE_CC_RETRIEVE(context, cache, mcreds, ret); + if (ret != KRB5_CC_NOTFOUND) + return ret; +- if (!krb5_is_referral_realm(&mcreds->server->realm)) ++ if (mcreds->client == NULL || mcreds->server == NULL || ++ !krb5_is_referral_realm(&mcreds->server->realm)) + return ret; + + /* +diff --git a/src/lib/krb5/krb/get_creds.c b/src/lib/krb5/krb/get_creds.c +index dc0aef667..b3f01be9b 100644 +--- a/src/lib/krb5/krb/get_creds.c ++++ b/src/lib/krb5/krb/get_creds.c +@@ -102,6 +102,11 @@ krb5int_construct_matching_creds(krb5_context context, krb5_flags options, + return KRB5_NO_2ND_TKT; + } + ++ /* For S4U2Proxy requests we don't know the impersonated client in this ++ * API, but matching against the second ticket is good enough. */ ++ if (options & KRB5_GC_CONSTRAINED_DELEGATION) ++ mcreds->client = NULL; ++ + return 0; + } + +diff --git a/src/lib/krb5/krb/s4u_creds.c b/src/lib/krb5/krb/s4u_creds.c +index 07c7a98be..d44f939f8 100644 +--- a/src/lib/krb5/krb/s4u_creds.c ++++ b/src/lib/krb5/krb/s4u_creds.c +@@ -1122,6 +1122,13 @@ get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, + code = KRB5KRB_AP_WRONG_PRINC; + goto cleanup; + } ++ ++ /* Put the original evidence ticket in the output creds. */ ++ krb5_free_data_contents(context, &tkt->second_ticket); ++ code = krb5int_copy_data_contents(context, &in_creds->second_ticket, ++ &tkt->second_ticket); ++ if (code) ++ goto cleanup; + } + + /* Note the authdata we asked for in the output creds. */ +@@ -1148,11 +1155,33 @@ k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, + { + krb5_error_code code; + krb5_const_principal canonprinc; +- krb5_creds copy, *creds; ++ krb5_creds mcreds, copy, *creds, *ncreds; ++ krb5_flags fields; + struct canonprinc iter = { in_creds->server, .no_hostrealm = TRUE }; + + *out_creds = NULL; + ++ code = krb5int_construct_matching_creds(context, options, in_creds, ++ &mcreds, &fields); ++ if (code != 0) ++ return code; ++ ++ ncreds = calloc(1, sizeof(*ncreds)); ++ if (ncreds == NULL) ++ return ENOMEM; ++ ncreds->magic = KV5M_CRED; ++ ++ code = krb5_cc_retrieve_cred(context, ccache, fields, &mcreds, ncreds); ++ if (code) { ++ free(ncreds); ++ } else { ++ *out_creds = ncreds; ++ } ++ ++ if ((code != KRB5_CC_NOTFOUND && code != KRB5_CC_NOT_KTYPE) || ++ options & KRB5_GC_CACHED) ++ return code; ++ + copy = *in_creds; + while ((code = k5_canonprinc(context, &iter, &canonprinc)) == 0 && + canonprinc != NULL) { +@@ -1198,9 +1227,6 @@ krb5_get_credentials_for_proxy(krb5_context context, + krb5_creds **out_creds) + { + krb5_error_code code; +- krb5_creds mcreds; +- krb5_creds *ncreds = NULL; +- krb5_flags fields; + krb5_data *evidence_tkt_data = NULL; + krb5_creds s4u_creds; + +@@ -1222,30 +1248,6 @@ krb5_get_credentials_for_proxy(krb5_context context, + goto cleanup; + } + +- code = krb5int_construct_matching_creds(context, options, in_creds, +- &mcreds, &fields); +- if (code != 0) +- goto cleanup; +- +- ncreds = calloc(1, sizeof(*ncreds)); +- if (ncreds == NULL) { +- code = ENOMEM; +- goto cleanup; +- } +- ncreds->magic = KV5M_CRED; +- +- code = krb5_cc_retrieve_cred(context, ccache, fields, &mcreds, ncreds); +- if (code != 0) { +- free(ncreds); +- ncreds = in_creds; +- } else { +- *out_creds = ncreds; +- } +- +- if ((code != KRB5_CC_NOTFOUND && code != KRB5_CC_NOT_KTYPE) +- || options & KRB5_GC_CACHED) +- goto cleanup; +- + code = encode_krb5_ticket(evidence_tkt, &evidence_tkt_data); + if (code != 0) + goto cleanup; +diff --git a/src/tests/s4u2proxy.c b/src/tests/s4u2proxy.c +index 4adf6aca5..3786bad2c 100644 +--- a/src/tests/s4u2proxy.c ++++ b/src/tests/s4u2proxy.c +@@ -124,6 +124,9 @@ main(int argc, char **argv) + KRB5_GC_CANONICALIZE, defcc, + &mcred, ev_ticket, &new_cred)); + ++ assert(data_eq(new_cred->second_ticket, ev_cred.ticket)); ++ assert(new_cred->second_ticket.length != 0); ++ + /* Store the new cred in the default ccache. */ + check(krb5_cc_store_cred(context, defcc, new_cred)); + diff --git a/Don-t-create-hostbased-principals-in-new-KDBs.patch b/Don-t-create-hostbased-principals-in-new-KDBs.patch new file mode 100644 index 0000000..867eb4a --- /dev/null +++ b/Don-t-create-hostbased-principals-in-new-KDBs.patch @@ -0,0 +1,195 @@ +From 71070a0f0fa6424cfb37aef7c4ec43e99380a6aa Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Thu, 30 Jul 2020 12:14:27 -0400 +Subject: [PATCH] Don't create hostbased principals in new KDBs + +Unix-like platforms do not provide a simple method to find the +fully-qualified local hostname as the machine is expected to appear to +other hosts. Canonicalizing the gethostname() result with +getaddrinfo() usually works, but potentially uses DNS. Now that +dns_canonicalize_hostname=true is no longer the default, KDB creation +would generally create the wrong host-based principals. + +kadmin/hostname is unnecessary because the client software can also +use kadmin/admin, and kiprop/hostname is one of several principals +that must be created for incremental propagation. + +ticket: 8935 (new) +(cherry picked from commit ac2b693d0ec464e0bcda4953acd79f201169f396) +--- + src/kadmin/dbutil/kadm5_create.c | 52 ++----------------- + .../kdb/ldap/ldap_util/kdb5_ldap_realm.c | 35 +------------ + src/tests/dejagnu/krb-standalone/kadmin.exp | 7 +-- + src/tests/t_iprop.py | 1 + + src/tests/t_kadmin_acl.py | 1 + + 5 files changed, 12 insertions(+), 84 deletions(-) + +diff --git a/src/kadmin/dbutil/kadm5_create.c b/src/kadmin/dbutil/kadm5_create.c +index 4f254a387..42b45aa2d 100644 +--- a/src/kadmin/dbutil/kadm5_create.c ++++ b/src/kadmin/dbutil/kadm5_create.c +@@ -139,60 +139,18 @@ int kadm5_create_magic_princs(kadm5_config_params *params, + static int add_admin_princs(void *handle, krb5_context context, char *realm) + { + krb5_error_code ret = 0; +- char *service_name = 0, *kiprop_name = 0, *canonhost = 0; +- char localname[MAXHOSTNAMELEN]; +- +- if (gethostname(localname, MAXHOSTNAMELEN)) { +- ret = errno; +- perror("gethostname"); +- goto clean_and_exit; +- } +- ret = krb5_expand_hostname(context, localname, &canonhost); +- if (ret) { +- com_err(progname, ret, _("while canonicalizing local hostname")); +- goto clean_and_exit; +- } +- if (asprintf(&service_name, "kadmin/%s", canonhost) < 0) { +- ret = ENOMEM; +- fprintf(stderr, _("Out of memory\n")); +- goto clean_and_exit; +- } +- if (asprintf(&kiprop_name, "kiprop/%s", canonhost) < 0) { +- ret = ENOMEM; +- fprintf(stderr, _("Out of memory\n")); +- goto clean_and_exit; +- } +- +- if ((ret = add_admin_princ(handle, context, +- service_name, realm, +- KRB5_KDB_DISALLOW_TGT_BASED | +- KRB5_KDB_LOCKDOWN_KEYS, +- ADMIN_LIFETIME))) +- goto clean_and_exit; + + if ((ret = add_admin_princ(handle, context, + KADM5_ADMIN_SERVICE, realm, + KRB5_KDB_DISALLOW_TGT_BASED | + KRB5_KDB_LOCKDOWN_KEYS, + ADMIN_LIFETIME))) +- goto clean_and_exit; ++ return ret; + +- if ((ret = add_admin_princ(handle, context, +- KADM5_CHANGEPW_SERVICE, realm, +- KRB5_KDB_DISALLOW_TGT_BASED | +- KRB5_KDB_PWCHANGE_SERVICE | +- KRB5_KDB_LOCKDOWN_KEYS, +- CHANGEPW_LIFETIME))) +- goto clean_and_exit; +- +- ret = add_admin_princ(handle, context, kiprop_name, realm, 0, 0); +- +-clean_and_exit: +- krb5_free_string(context, canonhost); +- free(service_name); +- free(kiprop_name); +- +- return ret; ++ return add_admin_princ(handle, context, KADM5_CHANGEPW_SERVICE, realm, ++ KRB5_KDB_DISALLOW_TGT_BASED | ++ KRB5_KDB_PWCHANGE_SERVICE | KRB5_KDB_LOCKDOWN_KEYS, ++ CHANGEPW_LIFETIME); + } + + /* +diff --git a/src/plugins/kdb/ldap/ldap_util/kdb5_ldap_realm.c b/src/plugins/kdb/ldap/ldap_util/kdb5_ldap_realm.c +index c21d19981..ae1afd4a9 100644 +--- a/src/plugins/kdb/ldap/ldap_util/kdb5_ldap_realm.c ++++ b/src/plugins/kdb/ldap/ldap_util/kdb5_ldap_realm.c +@@ -307,29 +307,6 @@ create_fixed_special(krb5_context context, struct realm_info *rinfo, + + } + +-/* Create a special principal using one specified component and the +- * canonicalized local hostname. */ +-static krb5_error_code +-create_hostbased_special(krb5_context context, struct realm_info *rinfo, +- krb5_keyblock *mkey, const char *comp1) +-{ +- krb5_error_code ret; +- krb5_principal princ = NULL; +- +- ret = krb5_sname_to_principal(context, NULL, comp1, KRB5_NT_SRV_HST, +- &princ); +- if (ret) +- goto cleanup; +- ret = krb5_set_principal_realm(context, princ, global_params.realm); +- if (ret) +- goto cleanup; +- ret = kdb_ldap_create_principal(context, princ, TGT_KEY, rinfo, mkey); +- +-cleanup: +- krb5_free_principal(context, princ); +- return ret; +-} +- + /* Create all special principals for the realm. */ + static krb5_error_code + create_special_princs(krb5_context context, krb5_principal master_princ, +@@ -360,20 +337,10 @@ create_special_princs(krb5_context context, krb5_principal master_princ, + if (ret) + return ret; + +- /* Create kadmin/admin and kadmin/. */ ++ /* Create kadmin/admin. */ + rblock.max_life = ADMIN_LIFETIME; + rblock.flags = KRB5_KDB_DISALLOW_TGT_BASED; + ret = create_fixed_special(context, &rblock, mkey, "kadmin", "admin"); +- if (ret) +- return ret; +- ret = create_hostbased_special(context, &rblock, mkey, "kadmin"); +- if (ret) +- return ret; +- +- /* Create kiprop/. */ +- rblock.max_life = global_params.max_life; +- rblock.flags = 0; +- ret = create_hostbased_special(context, &rblock, mkey, "kiprop"); + if (ret) + return ret; + +diff --git a/src/tests/dejagnu/krb-standalone/kadmin.exp b/src/tests/dejagnu/krb-standalone/kadmin.exp +index 36a345258..fa50a61fb 100644 +--- a/src/tests/dejagnu/krb-standalone/kadmin.exp ++++ b/src/tests/dejagnu/krb-standalone/kadmin.exp +@@ -1098,10 +1098,11 @@ proc kadmin_test { } { + return + } + +- # test fallback to kadmin/admin +- if {![kadmin_delete_locked_down kadmin/$hostname] \ ++ # test fallback to kadmin/hostname ++ if {![kadmin_add_rnd kadmin/$hostname] \ ++ || ![kadmin_delete_locked_down kadmin/admin] \ + || ![kadmin_list] \ +- || ![kadmin_add_rnd kadmin/$hostname -allow_tgs_req] \ ++ || ![kadmin_add_rnd kadmin/admin -allow_tgs_req] \ + || ![kadmin_list]} { + return + } +diff --git a/src/tests/t_iprop.py b/src/tests/t_iprop.py +index 371f3a22b..3bb0fd2e9 100755 +--- a/src/tests/t_iprop.py ++++ b/src/tests/t_iprop.py +@@ -188,6 +188,7 @@ for realm in multidb_realms(kdc_conf=conf, create_user=False, + + # Create the principal used to authenticate kpropd to kadmind. + kiprop_princ = 'kiprop/' + hostname ++ realm.addprinc(kiprop_princ) + realm.extract_keytab(kiprop_princ, realm.keytab) + + # Create the initial replica databases. +diff --git a/src/tests/t_kadmin_acl.py b/src/tests/t_kadmin_acl.py +index 16faf0a9d..31a7fb871 100755 +--- a/src/tests/t_kadmin_acl.py ++++ b/src/tests/t_kadmin_acl.py +@@ -331,6 +331,7 @@ realm.run([kadmin, '-c', realm.ccache, 'cpw', '-randkey', '-e', 'aes256-cts', + # Test authentication to kadmin/hostname. + mark('authentication to kadmin/hostname') + kadmin_hostname = 'kadmin/' + hostname ++realm.addprinc(kadmin_hostname) + realm.run([kadminl, 'delprinc', 'kadmin/admin']) + msgs = ('Getting initial credentials for user/admin@KRBTEST.COM', + 'Setting initial creds service to kadmin/admin', diff --git a/Expand-dns_canonicalize_host-fallback-support.patch b/Expand-dns_canonicalize_host-fallback-support.patch new file mode 100644 index 0000000..eb82a81 --- /dev/null +++ b/Expand-dns_canonicalize_host-fallback-support.patch @@ -0,0 +1,1190 @@ +From c0e732f79dc5ea0c2066120bfe7ae8f6df82bf82 Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Fri, 17 Jul 2020 22:57:45 -0400 +Subject: [PATCH] Expand dns_canonicalize_host=fallback support + +In krb5_sname_to_principal(), when using fallback, defer realm lookup +and any kind of hostname canonicalization until use. Add a +lightweight iterator k5_canonprinc() to yield the one or two possible +candidates for a principal. In the iterator, don't yield the same +hostname part twice. + +Add fallback processing to the stepwise TGS state machine, and remove +it from krb5_get_credentials(). Add fallback processing to +k5_get_proxy_cred_from_kdc(). + +Add fallback processing to krb5_init_creds_set_keytab(), and use the +principal we find in the keytab as the request client principal. +Defer restart_init_creds_loop() to the first step call so that server +principal is built using the correct realm. + +Add fallback processing to krb5_rd_req(). + +ticket: 8930 (new) +(cherry picked from commit 3fcc365a6f049730b3f47168f7112c03997c5c0b) +--- + src/include/k5-trace.h | 4 +- + src/kprop/kprop_util.c | 26 ++-- + src/lib/krb5/krb/deps | 41 +++--- + src/lib/krb5/krb/get_creds.c | 151 ++++++++++----------- + src/lib/krb5/krb/get_in_tkt.c | 7 +- + src/lib/krb5/krb/gic_keytab.c | 29 +++- + src/lib/krb5/krb/init_creds_ctx.h | 1 + + src/lib/krb5/krb/rd_req_dec.c | 36 ++++- + src/lib/krb5/krb/s4u_creds.c | 62 ++++++--- + src/lib/krb5/os/os-proto.h | 30 +++++ + src/lib/krb5/os/sn2princ.c | 215 +++++++++++++++++++++--------- + src/tests/icred.c | 39 ++++-- + src/tests/t_sn2princ.py | 65 ++++++--- + 13 files changed, 465 insertions(+), 241 deletions(-) + +diff --git a/src/include/k5-trace.h b/src/include/k5-trace.h +index 1da53dbb1..5a120f1a0 100644 +--- a/src/include/k5-trace.h ++++ b/src/include/k5-trace.h +@@ -229,8 +229,8 @@ void krb5int_trace(krb5_context context, const char *fmt, ...); + salt, s2kparams) + #define TRACE_INIT_CREDS_IDENTIFIED_REALM(c, realm) \ + TRACE(c, "Identified realm of client principal as {data}", realm) +-#define TRACE_INIT_CREDS_KEYTAB_LOOKUP(c, etypes) \ +- TRACE(c, "Looked up etypes in keytab: {etypes}", etypes) ++#define TRACE_INIT_CREDS_KEYTAB_LOOKUP(c, princ, etypes) \ ++ TRACE(c, "Found entries for {princ} in keytab: {etypes}", princ, etypes) + #define TRACE_INIT_CREDS_KEYTAB_LOOKUP_FAILED(c, code) \ + TRACE(c, "Couldn't lookup etypes in keytab: {kerr}", code) + #define TRACE_INIT_CREDS_PREAUTH(c) \ +diff --git a/src/kprop/kprop_util.c b/src/kprop/kprop_util.c +index c32d174b9..c2b2e8764 100644 +--- a/src/kprop/kprop_util.c ++++ b/src/kprop/kprop_util.c +@@ -73,26 +73,22 @@ sn2princ_realm(krb5_context context, const char *hostname, const char *sname, + const char *realm, krb5_principal *princ_out) + { + krb5_error_code ret; +- char *canonhost, localname[MAXHOSTNAMELEN]; ++ krb5_principal princ; + + *princ_out = NULL; + assert(sname != NULL && realm != NULL); + +- /* If hostname is NULL, use the local hostname. */ +- if (hostname == NULL) { +- if (gethostname(localname, MAXHOSTNAMELEN) != 0) +- return SOCKET_ERRNO; +- hostname = localname; +- } +- +- ret = krb5_expand_hostname(context, hostname, &canonhost); ++ ret = krb5_sname_to_principal(context, hostname, sname, KRB5_NT_SRV_HST, ++ &princ); + if (ret) + return ret; + +- ret = krb5_build_principal(context, princ_out, strlen(realm), realm, sname, +- canonhost, (char *)NULL); +- krb5_free_string(context, canonhost); +- if (!ret) +- (*princ_out)->type = KRB5_NT_SRV_HST; +- return ret; ++ ret = krb5_set_principal_realm(context, princ, realm); ++ if (ret) { ++ krb5_free_principal(context, princ); ++ return ret; ++ } ++ ++ *princ_out = princ; ++ return 0; + } +diff --git a/src/lib/krb5/krb/deps b/src/lib/krb5/krb/deps +index 439ca0272..6ac68bc19 100644 +--- a/src/lib/krb5/krb/deps ++++ b/src/lib/krb5/krb/deps +@@ -499,12 +499,13 @@ get_in_tkt.so get_in_tkt.po $(OUTPRE)get_in_tkt.$(OBJEXT): \ + gic_keytab.so gic_keytab.po $(OUTPRE)gic_keytab.$(OBJEXT): \ + $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \ + $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \ +- $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \ +- $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \ +- $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-json.h \ +- $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ +- $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ +- $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ ++ $(COM_ERR_DEPS) $(srcdir)/../os/os-proto.h $(top_srcdir)/include/k5-buf.h \ ++ $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ ++ $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ ++ $(top_srcdir)/include/k5-json.h $(top_srcdir)/include/k5-platform.h \ ++ $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \ ++ $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krb5.h \ ++ $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/locate_plugin.h \ + $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \ + $(top_srcdir)/include/socket-utils.h gic_keytab.c init_creds_ctx.h \ + int-proto.h +@@ -940,13 +941,14 @@ rd_req.so rd_req.po $(OUTPRE)rd_req.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ + rd_req_dec.so rd_req_dec.po $(OUTPRE)rd_req_dec.$(OBJEXT): \ + $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \ + $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \ +- $(COM_ERR_DEPS) $(srcdir)/../rcache/memrcache.h $(top_srcdir)/include/k5-buf.h \ +- $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ +- $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ +- $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ +- $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ +- $(top_srcdir)/include/k5-utf8.h $(top_srcdir)/include/krb5.h \ +- $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \ ++ $(COM_ERR_DEPS) $(srcdir)/../os/os-proto.h $(srcdir)/../rcache/memrcache.h \ ++ $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \ ++ $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \ ++ $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \ ++ $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \ ++ $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/k5-utf8.h \ ++ $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ ++ $(top_srcdir)/include/krb5/locate_plugin.h $(top_srcdir)/include/krb5/plugin.h \ + $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \ + auth_con.h authdata.h int-proto.h rd_req_dec.c + rd_safe.so rd_safe.po $(OUTPRE)rd_safe.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ +@@ -997,12 +999,13 @@ s4u_authdata.so s4u_authdata.po $(OUTPRE)s4u_authdata.$(OBJEXT): \ + s4u_creds.so s4u_creds.po $(OUTPRE)s4u_creds.$(OBJEXT): \ + $(BUILDTOP)/include/autoconf.h $(BUILDTOP)/include/krb5/krb5.h \ + $(BUILDTOP)/include/osconf.h $(BUILDTOP)/include/profile.h \ +- $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h $(top_srcdir)/include/k5-err.h \ +- $(top_srcdir)/include/k5-gmt_mktime.h $(top_srcdir)/include/k5-int-pkinit.h \ +- $(top_srcdir)/include/k5-int.h $(top_srcdir)/include/k5-platform.h \ +- $(top_srcdir)/include/k5-plugin.h $(top_srcdir)/include/k5-thread.h \ +- $(top_srcdir)/include/k5-trace.h $(top_srcdir)/include/krb5.h \ +- $(top_srcdir)/include/krb5/authdata_plugin.h $(top_srcdir)/include/krb5/plugin.h \ ++ $(COM_ERR_DEPS) $(srcdir)/../os/os-proto.h $(top_srcdir)/include/k5-buf.h \ ++ $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ ++ $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ ++ $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ ++ $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ ++ $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ ++ $(top_srcdir)/include/krb5/locate_plugin.h $(top_srcdir)/include/krb5/plugin.h \ + $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \ + int-proto.h s4u_creds.c + sendauth.so sendauth.po $(OUTPRE)sendauth.$(OBJEXT): \ +diff --git a/src/lib/krb5/krb/get_creds.c b/src/lib/krb5/krb/get_creds.c +index e0a3b5cd8..dc0aef667 100644 +--- a/src/lib/krb5/krb/get_creds.c ++++ b/src/lib/krb5/krb/get_creds.c +@@ -119,7 +119,7 @@ krb5int_construct_matching_creds(krb5_context context, krb5_flags options, + * generate the next request. If it's time to advance to another state, any of + * the three functions can make a tail call to begin_ to do so. + * +- * The overall process is as follows: ++ * The general process is as follows: + * 1. Get a TGT for the service principal's realm (STATE_GET_TGT). + * 2. Make one or more referrals queries (STATE_REFERRALS). + * 3. In some cases, get a TGT for the fallback realm (STATE_GET_TGT again). +@@ -129,6 +129,9 @@ krb5int_construct_matching_creds(krb5_context context, krb5_flags options, + * getting_tgt_for field in the context keeps track of what state we will go to + * after successfully obtaining the TGT, and the end_get_tgt() function + * advances to the proper next state. ++ * ++ * If fallback DNS canonicalization is in use, the process can be repeated a ++ * second time for the second server principal canonicalization candidate. + */ + + enum state { +@@ -153,6 +156,8 @@ struct _krb5_tkt_creds_context { + krb5_flags req_options; /* Caller-requested KRB5_GC_* options */ + krb5_flags req_kdcopt; /* Caller-requested options as KDC options */ + krb5_authdata **authdata; /* Caller-requested authdata */ ++ struct canonprinc iter; /* Iterator over canonicalized server princs */ ++ krb5_boolean referral_req; /* Server initially contained referral realm */ + + /* The following fields are used in multiple steps. */ + krb5_creds *cur_tgt; /* TGT to be used for next query */ +@@ -484,7 +489,7 @@ try_fallback(krb5_context context, krb5_tkt_creds_context ctx) + + /* If the request used a specified realm, make a non-referral request to + * that realm (in case it's a KDC which rejects KDC_OPT_CANONICALIZE). */ +- if (!krb5_is_referral_realm(&ctx->req_server->realm)) ++ if (!ctx->referral_req) + return begin_non_referral(context, ctx); + + if (ctx->server->length < 2) { +@@ -1015,10 +1020,13 @@ check_cache(krb5_context context, krb5_tkt_creds_context ctx) + krb5_error_code code; + krb5_creds mcreds; + krb5_flags fields; ++ krb5_creds req_in_creds; + +- /* Perform the cache lookup. */ ++ /* Check the cache for the originally requested server principal. */ ++ req_in_creds = *ctx->in_creds; ++ req_in_creds.server = ctx->req_server; + code = krb5int_construct_matching_creds(context, ctx->req_options, +- ctx->in_creds, &mcreds, &fields); ++ &req_in_creds, &mcreds, &fields); + if (code) + return code; + code = cache_get(context, ctx->ccache, fields, &mcreds, &ctx->reply_creds); +@@ -1044,12 +1052,9 @@ begin(krb5_context context, krb5_tkt_creds_context ctx) + { + krb5_error_code code; + +- code = check_cache(context, ctx); +- if (code != 0 || ctx->state == STATE_COMPLETE) +- return code; +- + /* If the server realm is unspecified, start with the client realm. */ +- if (krb5_is_referral_realm(&ctx->server->realm)) { ++ ctx->referral_req = krb5_is_referral_realm(&ctx->server->realm); ++ if (ctx->referral_req) { + krb5_free_data_contents(context, &ctx->server->realm); + code = krb5int_copy_data_contents(context, &ctx->client->realm, + &ctx->server->realm); +@@ -1072,6 +1077,7 @@ krb5_tkt_creds_init(krb5_context context, krb5_ccache ccache, + { + krb5_error_code code; + krb5_tkt_creds_context ctx = NULL; ++ krb5_const_principal canonprinc; + + TRACE_TKT_CREDS(context, in_creds, ccache); + ctx = k5alloc(sizeof(*ctx), &code); +@@ -1089,14 +1095,28 @@ krb5_tkt_creds_init(krb5_context context, krb5_ccache ccache, + + ctx->state = STATE_BEGIN; + ++ /* Copy the matching cred so we can modify it. Steal the copy of the ++ * service principal name to remember the original request server. */ + code = krb5_copy_creds(context, in_creds, &ctx->in_creds); + if (code != 0) + goto cleanup; +- ctx->client = ctx->in_creds->client; +- ctx->server = ctx->in_creds->server; +- code = krb5_copy_principal(context, ctx->server, &ctx->req_server); ++ ctx->req_server = ctx->in_creds->server; ++ ctx->in_creds->server = NULL; ++ ++ /* Get the first canonicalization candidate for the requested server. */ ++ ctx->iter.princ = ctx->req_server; ++ ++ code = k5_canonprinc(context, &ctx->iter, &canonprinc); ++ if (code == 0 && canonprinc == NULL) ++ code = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; + if (code != 0) + goto cleanup; ++ code = krb5_copy_principal(context, canonprinc, &ctx->in_creds->server); ++ if (code != 0) ++ goto cleanup; ++ ++ ctx->client = ctx->in_creds->client; ++ ctx->server = ctx->in_creds->server; + code = krb5_cc_dup(context, ccache, &ctx->ccache); + if (code != 0) + goto cleanup; +@@ -1138,6 +1158,7 @@ krb5_tkt_creds_free(krb5_context context, krb5_tkt_creds_context ctx) + return; + krb5int_fast_free_state(context, ctx->fast_state); + krb5_free_creds(context, ctx->in_creds); ++ free_canonprinc(&ctx->iter); + krb5_cc_close(context, ctx->ccache); + krb5_free_principal(context, ctx->req_server); + krb5_free_authdata(context, ctx->authdata); +@@ -1195,6 +1216,7 @@ krb5_tkt_creds_step(krb5_context context, krb5_tkt_creds_context ctx, + { + krb5_error_code code; + krb5_boolean no_input = (in == NULL || in->length == 0); ++ krb5_const_principal canonprinc; + + *out = empty_data(); + *realm = empty_data(); +@@ -1206,6 +1228,12 @@ krb5_tkt_creds_step(krb5_context context, krb5_tkt_creds_context ctx, + ctx->state == STATE_COMPLETE) + return EINVAL; + ++ if (ctx->state == STATE_BEGIN) { ++ code = check_cache(context, ctx); ++ if (code != 0 || ctx->state == STATE_COMPLETE) ++ return code; ++ } ++ + ctx->caller_out = out; + ctx->caller_realm = realm; + ctx->caller_flags = flags; +@@ -1218,37 +1246,32 @@ krb5_tkt_creds_step(krb5_context context, krb5_tkt_creds_context ctx, + } + + if (ctx->state == STATE_BEGIN) +- return begin(context, ctx); ++ code = begin(context, ctx); + else if (ctx->state == STATE_GET_TGT) +- return step_get_tgt(context, ctx); ++ code = step_get_tgt(context, ctx); + else if (ctx->state == STATE_GET_TGT_OFFPATH) +- return step_get_tgt_offpath(context, ctx); ++ code = step_get_tgt_offpath(context, ctx); + else if (ctx->state == STATE_REFERRALS) +- return step_referrals(context, ctx); ++ code = step_referrals(context, ctx); + else if (ctx->state == STATE_NON_REFERRAL) +- return step_non_referral(context, ctx); ++ code = step_non_referral(context, ctx); + else +- return EINVAL; +-} ++ code = EINVAL; + +-static krb5_error_code +-try_get_creds(krb5_context context, krb5_flags options, krb5_ccache ccache, +- krb5_creds *in_creds, krb5_creds *creds_out) +-{ +- krb5_error_code code; +- krb5_tkt_creds_context ctx = NULL; ++ /* Terminate on success or most errors. */ ++ if (code != KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN) ++ return code; + +- code = krb5_tkt_creds_init(context, ccache, in_creds, options, &ctx); ++ /* Restart with the next server principal canonicalization candidate. */ ++ code = k5_canonprinc(context, &ctx->iter, &canonprinc); + if (code) +- goto cleanup; +- code = krb5_tkt_creds_get(context, ctx); +- if (code) +- goto cleanup; +- code = krb5_tkt_creds_get_creds(context, ctx, creds_out); +- +-cleanup: +- krb5_tkt_creds_free(context, ctx); +- return code; ++ return code; ++ if (canonprinc == NULL) ++ return KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; ++ krb5_free_principal(context, ctx->in_creds->server); ++ code = krb5_copy_principal(context, canonprinc, &ctx->in_creds->server); ++ ctx->server = ctx->in_creds->server; ++ return begin(context, ctx); + } + + krb5_error_code KRB5_CALLCONV +@@ -1258,10 +1281,7 @@ krb5_get_credentials(krb5_context context, krb5_flags options, + { + krb5_error_code code; + krb5_creds *ncreds = NULL; +- krb5_creds canon_creds, store_creds; +- krb5_principal_data canon_server; +- krb5_data canon_components[2]; +- char *hostname = NULL, *canon_hostname = NULL; ++ krb5_tkt_creds_context ctx = NULL; + + *out_creds = NULL; + +@@ -1277,59 +1297,22 @@ krb5_get_credentials(krb5_context context, krb5_flags options, + if (ncreds == NULL) + goto cleanup; + +- code = try_get_creds(context, options, ccache, in_creds, ncreds); +- if (!code) { +- *out_creds = ncreds; +- return 0; +- } +- +- /* Possibly try again with the canonicalized hostname, if the server is +- * host-based and we are configured for fallback canonicalization. */ +- if (code != KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN) ++ /* Make and execute a krb5_tkt_creds context to get the credential. */ ++ code = krb5_tkt_creds_init(context, ccache, in_creds, options, &ctx); ++ if (code != 0) + goto cleanup; +- if (context->dns_canonicalize_hostname != CANONHOST_FALLBACK) ++ code = krb5_tkt_creds_get(context, ctx); ++ if (code != 0) + goto cleanup; +- if (in_creds->server->type != KRB5_NT_SRV_HST || +- in_creds->server->length != 2) ++ code = krb5_tkt_creds_get_creds(context, ctx, ncreds); ++ if (code != 0) + goto cleanup; + +- hostname = k5memdup0(in_creds->server->data[1].data, +- in_creds->server->data[1].length, &code); +- if (hostname == NULL) +- goto cleanup; +- code = k5_expand_hostname(context, hostname, TRUE, &canon_hostname); +- if (code) +- goto cleanup; +- +- TRACE_GET_CREDS_FALLBACK(context, canon_hostname); +- +- /* Make shallow copies of in_creds and its server to alter the hostname. */ +- canon_components[0] = in_creds->server->data[0]; +- canon_components[1] = string2data(canon_hostname); +- canon_server = *in_creds->server; +- canon_server.data = canon_components; +- canon_creds = *in_creds; +- canon_creds.server = &canon_server; +- +- code = try_get_creds(context, options | KRB5_GC_NO_STORE, ccache, +- &canon_creds, ncreds); +- if (code) +- goto cleanup; +- +- if (!(options & KRB5_GC_NO_STORE)) { +- /* Store the creds under the originally requested server name. The +- * ccache layer will also store them under the ticket server name. */ +- store_creds = *ncreds; +- store_creds.server = in_creds->server; +- (void)krb5_cc_store_cred(context, ccache, &store_creds); +- } +- + *out_creds = ncreds; + ncreds = NULL; + + cleanup: +- free(hostname); +- free(canon_hostname); + krb5_free_creds(context, ncreds); ++ krb5_tkt_creds_free(context, ctx); + return code; + } +diff --git a/src/lib/krb5/krb/get_in_tkt.c b/src/lib/krb5/krb/get_in_tkt.c +index cc0f70e83..09c4b8495 100644 +--- a/src/lib/krb5/krb/get_in_tkt.c ++++ b/src/lib/krb5/krb/get_in_tkt.c +@@ -1051,9 +1051,6 @@ krb5_init_creds_init(krb5_context context, + ctx->request->kdc_options |= KDC_OPT_REQUEST_ANONYMOUS; + ctx->request->client->type = KRB5_NT_WELLKNOWN; + } +- code = restart_init_creds_loop(context, ctx, FALSE); +- if (code) +- goto cleanup; + + *pctx = ctx; + ctx = NULL; +@@ -1859,6 +1856,10 @@ krb5_init_creds_step(krb5_context context, + } + if (code != 0 || ctx->complete) + goto cleanup; ++ } else { ++ code = restart_init_creds_loop(context, ctx, FALSE); ++ if (code) ++ goto cleanup; + } + + code = init_creds_step_request(context, ctx, out); +diff --git a/src/lib/krb5/krb/gic_keytab.c b/src/lib/krb5/krb/gic_keytab.c +index 1d70cf46f..b2b4ac904 100644 +--- a/src/lib/krb5/krb/gic_keytab.c ++++ b/src/lib/krb5/krb/gic_keytab.c +@@ -27,6 +27,7 @@ + + #include "k5-int.h" + #include "int-proto.h" ++#include "os-proto.h" + #include "init_creds_ctx.h" + + static krb5_error_code +@@ -85,7 +86,8 @@ get_as_key_keytab(krb5_context context, + /* Return the list of etypes available for client in keytab. */ + static krb5_error_code + lookup_etypes_for_keytab(krb5_context context, krb5_keytab keytab, +- krb5_principal client, krb5_enctype **etypes_out) ++ krb5_const_principal client, ++ krb5_enctype **etypes_out) + { + krb5_kt_cursor cursor; + krb5_keytab_entry entry; +@@ -182,18 +184,37 @@ krb5_init_creds_set_keytab(krb5_context context, + { + krb5_enctype *etype_list; + krb5_error_code ret; ++ struct canonprinc iter = { ctx->request->client, .subst_defrealm = TRUE }; ++ krb5_const_principal canonprinc; ++ krb5_principal copy; + char *name; + + ctx->gak_fct = get_as_key_keytab; + ctx->gak_data = keytab; + +- ret = lookup_etypes_for_keytab(context, keytab, ctx->request->client, +- &etype_list); ++ /* We may be authenticating as a host-based principal. If so, look for ++ * each canonicalization candidate in the keytab. */ ++ while ((ret = k5_canonprinc(context, &iter, &canonprinc)) == 0 && ++ canonprinc != NULL) { ++ ret = lookup_etypes_for_keytab(context, keytab, canonprinc, ++ &etype_list); ++ if (ret || etype_list != NULL) ++ break; ++ } ++ if (!ret && canonprinc != NULL) { ++ /* Authenticate as the principal we found in the keytab. */ ++ ret = krb5_copy_principal(context, canonprinc, ©); ++ if (!ret) { ++ krb5_free_principal(context, ctx->request->client); ++ ctx->request->client = copy; ++ } ++ } ++ free_canonprinc(&iter); + if (ret) { + TRACE_INIT_CREDS_KEYTAB_LOOKUP_FAILED(context, ret); + return 0; + } +- TRACE_INIT_CREDS_KEYTAB_LOOKUP(context, etype_list); ++ TRACE_INIT_CREDS_KEYTAB_LOOKUP(context, ctx->request->client, etype_list); + + /* Error out if we have no keys for the client principal. */ + if (etype_list == NULL) { +diff --git a/src/lib/krb5/krb/init_creds_ctx.h b/src/lib/krb5/krb/init_creds_ctx.h +index 5bd67a1d8..17d55dd7c 100644 +--- a/src/lib/krb5/krb/init_creds_ctx.h ++++ b/src/lib/krb5/krb/init_creds_ctx.h +@@ -22,6 +22,7 @@ struct _krb5_init_creds_context { + krb5_get_init_creds_opt opt_storage; + krb5_boolean identify_realm; + const krb5_data *subject_cert; ++ krb5_principal keytab_princ; + char *in_tkt_service; + krb5_prompter_fct prompter; + void *prompter_data; +diff --git a/src/lib/krb5/krb/rd_req_dec.c b/src/lib/krb5/krb/rd_req_dec.c +index bc7fac455..013ca905c 100644 +--- a/src/lib/krb5/krb/rd_req_dec.c ++++ b/src/lib/krb5/krb/rd_req_dec.c +@@ -33,6 +33,7 @@ + #include "auth_con.h" + #include "authdata.h" + #include "int-proto.h" ++#include "os-proto.h" + + /* + * essentially the same as krb_rd_req, but uses a decoded AP_REQ as +@@ -351,9 +352,9 @@ try_one_princ(krb5_context context, const krb5_ap_req *req, + * Store the decrypting key in *keyblock_out if it is not NULL. + */ + static krb5_error_code +-decrypt_ticket(krb5_context context, const krb5_ap_req *req, +- krb5_const_principal server, krb5_keytab keytab, +- krb5_keyblock *keyblock_out) ++decrypt_try_server(krb5_context context, const krb5_ap_req *req, ++ krb5_const_principal server, krb5_keytab keytab, ++ krb5_keyblock *keyblock_out) + { + krb5_error_code ret; + krb5_keytab_entry ent; +@@ -441,6 +442,35 @@ decrypt_ticket(krb5_context context, const krb5_ap_req *req, + #endif /* LEAN_CLIENT */ + } + ++static krb5_error_code ++decrypt_ticket(krb5_context context, const krb5_ap_req *req, ++ krb5_const_principal server, krb5_keytab keytab, ++ krb5_keyblock *keyblock_out) ++{ ++ krb5_error_code ret, dret = 0; ++ struct canonprinc iter = { server, .no_hostrealm = TRUE }; ++ krb5_const_principal canonprinc; ++ ++ /* Don't try to canonicalize if we're going to ignore the hostname, or if ++ * server is null or has a wildcard hostname. */ ++ if (context->ignore_acceptor_hostname || server == NULL || ++ (server->length == 2 && server->data[1].length == 0)) ++ return decrypt_try_server(context, req, server, keytab, keyblock_out); ++ ++ /* Try each canonicalization candidate for server. If they all fail, ++ * return the error from the last attempt. */ ++ while ((ret = k5_canonprinc(context, &iter, &canonprinc)) == 0 && ++ canonprinc != NULL) { ++ dret = decrypt_try_server(context, req, canonprinc, keytab, ++ keyblock_out); ++ /* Only continue if we found no keytab entries matching canonprinc. */ ++ if (dret != KRB5KRB_AP_ERR_NOKEY) ++ break; ++ } ++ free_canonprinc(&iter); ++ return (ret != 0) ? ret : dret; ++} ++ + static krb5_error_code + rd_req_decoded_opt(krb5_context context, krb5_auth_context *auth_context, + const krb5_ap_req *req, krb5_const_principal server, +diff --git a/src/lib/krb5/krb/s4u_creds.c b/src/lib/krb5/krb/s4u_creds.c +index d8f486dc6..07c7a98be 100644 +--- a/src/lib/krb5/krb/s4u_creds.c ++++ b/src/lib/krb5/krb/s4u_creds.c +@@ -26,6 +26,7 @@ + + #include "k5-int.h" + #include "int-proto.h" ++#include "os-proto.h" + + /* Convert ticket flags to necessary KDC options */ + #define FLAGS2OPTS(flags) (flags & KDC_TKT_COMMON_MASK) +@@ -984,10 +985,10 @@ get_target_realm_proxy_tgt(krb5_context context, const krb5_data *realm, + return 0; + } + +-krb5_error_code +-k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, +- krb5_ccache ccache, krb5_creds *in_creds, +- krb5_creds **out_creds) ++static krb5_error_code ++get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, ++ krb5_ccache ccache, krb5_creds *in_creds, ++ krb5_creds **out_creds) + { + krb5_error_code code; + krb5_flags flags, req_kdcopt = 0; +@@ -1123,22 +1124,11 @@ k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, + } + } + +- if (!krb5_principal_compare(context, in_creds->server, tkt->server)) { +- krb5_free_principal(context, tkt->server); +- tkt->server = NULL; +- code = krb5_copy_principal(context, in_creds->server, &tkt->server); +- if (code) +- goto cleanup; +- } +- + /* Note the authdata we asked for in the output creds. */ + code = krb5_copy_authdata(context, in_creds->authdata, &tkt->authdata); + if (code) + goto cleanup; + +- if (!(options & KRB5_GC_NO_STORE)) +- (void)krb5_cc_store_cred(context, ccache, tkt); +- + *out_creds = tkt; + tkt = NULL; + +@@ -1151,6 +1141,48 @@ cleanup: + return code; + } + ++krb5_error_code ++k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, ++ krb5_ccache ccache, krb5_creds *in_creds, ++ krb5_creds **out_creds) ++{ ++ krb5_error_code code; ++ krb5_const_principal canonprinc; ++ krb5_creds copy, *creds; ++ struct canonprinc iter = { in_creds->server, .no_hostrealm = TRUE }; ++ ++ *out_creds = NULL; ++ ++ copy = *in_creds; ++ while ((code = k5_canonprinc(context, &iter, &canonprinc)) == 0 && ++ canonprinc != NULL) { ++ copy.server = (krb5_principal)canonprinc; ++ code = get_proxy_cred_from_kdc(context, options, ccache, ©, ++ &creds); ++ if (code != KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN) ++ break; ++ } ++ if (!code && canonprinc == NULL) ++ code = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; ++ free_canonprinc(&iter); ++ if (code) ++ return code; ++ ++ krb5_free_principal(context, creds->server); ++ creds->server = NULL; ++ code = krb5_copy_principal(context, in_creds->server, &creds->server); ++ if (code) { ++ krb5_free_creds(context, creds); ++ return code; ++ } ++ ++ if (!(options & KRB5_GC_NO_STORE)) ++ (void)krb5_cc_store_cred(context, ccache, creds); ++ ++ *out_creds = creds; ++ return 0; ++} ++ + /* + * Exported API for constrained delegation (S4U2Proxy). + * +diff --git a/src/lib/krb5/os/os-proto.h b/src/lib/krb5/os/os-proto.h +index a16a34b74..f1aa60a3e 100644 +--- a/src/lib/krb5/os/os-proto.h ++++ b/src/lib/krb5/os/os-proto.h +@@ -83,6 +83,36 @@ struct sendto_callback_info { + void *data; + }; + ++/* ++ * Initialize with all zeros except for princ. Set no_hostrealm to disable ++ * host-to-realm lookup, which ordinarily happens after canonicalizing the host ++ * part. Set subst_defrealm to substitute the default realm for the referral ++ * realm after realm lookup (this has no effect if no_hostrealm is set). Free ++ * with free_canonprinc() when done. ++ */ ++struct canonprinc { ++ krb5_const_principal princ; ++ krb5_boolean no_hostrealm; ++ krb5_boolean subst_defrealm; ++ int step; ++ char *canonhost; ++ char *realm; ++ krb5_principal_data copy; ++ krb5_data components[2]; ++}; ++ ++/* Yield one or two candidate canonical principal names for iter, then NULL. ++ * Output names are valid for one iteration and must not be freed. */ ++krb5_error_code k5_canonprinc(krb5_context context, struct canonprinc *iter, ++ krb5_const_principal *princ_out); ++ ++static inline void ++free_canonprinc(struct canonprinc *iter) ++{ ++ free(iter->canonhost); ++ free(iter->realm); ++} ++ + krb5_error_code k5_expand_hostname(krb5_context context, const char *host, + krb5_boolean is_fallback, + char **canonhost_out); +diff --git a/src/lib/krb5/os/sn2princ.c b/src/lib/krb5/os/sn2princ.c +index a51761d0c..8b7214189 100644 +--- a/src/lib/krb5/os/sn2princ.c ++++ b/src/lib/krb5/os/sn2princ.c +@@ -85,22 +85,18 @@ qualify_shortname(krb5_context context, const char *host) + return fqdn; + } + +-krb5_error_code +-k5_expand_hostname(krb5_context context, const char *host, +- krb5_boolean is_fallback, char **canonhost_out) ++static krb5_error_code ++expand_hostname(krb5_context context, const char *host, krb5_boolean use_dns, ++ char **canonhost_out) + { + struct addrinfo *ai = NULL, hint; + char namebuf[NI_MAXHOST], *qualified = NULL, *copy, *p; + int err; + const char *canonhost; +- krb5_boolean use_dns; + + *canonhost_out = NULL; + + canonhost = host; +- use_dns = (context->dns_canonicalize_hostname == CANONHOST_TRUE || +- (is_fallback && +- context->dns_canonicalize_hostname == CANONHOST_FALLBACK)); + if (use_dns) { + /* Try a forward lookup of the hostname. */ + memset(&hint, 0, sizeof(hint)); +@@ -161,21 +157,135 @@ krb5_error_code KRB5_CALLCONV + krb5_expand_hostname(krb5_context context, const char *host, + char **canonhost_out) + { +- return k5_expand_hostname(context, host, FALSE, canonhost_out); ++ int use_dns = (context->dns_canonicalize_hostname == CANONHOST_TRUE); ++ ++ return expand_hostname(context, host, use_dns, canonhost_out); + } + +-/* If hostname appears to have a :port or :instance trailer (used in MSSQLSvc +- * principals), return a pointer to the separator. Otherwise return NULL. */ +-static const char * +-find_trailer(const char *hostname) ++/* Split data into hostname and trailer (:port or :instance). Trailers are ++ * used in MSSQLSvc principals. */ ++static void ++split_trailer(const krb5_data *data, krb5_data *host, krb5_data *trailer) + { +- const char *p = strchr(hostname, ':'); ++ char *p = memchr(data->data, ':', data->length); ++ unsigned int tlen = (p == NULL) ? 0 : data->length - (p - data->data); + +- /* Look for a single colon followed by one or more characters. An IPv6 +- * address will have more than one colon, so don't accept that. */ +- if (p == NULL || p[1] == '\0' || strchr(p + 1, ':') != NULL) +- return NULL; +- return p; ++ /* Make sure we have a single colon followed by one or more characters. An ++ * IPv6 address will have more than one colon, so don't accept that. */ ++ if (p == NULL || tlen == 1 || memchr(p + 1, ':', tlen - 1) != NULL) { ++ *host = *data; ++ *trailer = empty_data(); ++ } else { ++ *host = make_data(data->data, p - data->data); ++ *trailer = make_data(p, tlen); ++ } ++} ++ ++static krb5_error_code ++canonicalize_princ(krb5_context context, struct canonprinc *iter, ++ krb5_boolean use_dns, krb5_const_principal *princ_out) ++{ ++ krb5_error_code ret; ++ krb5_data host, trailer; ++ char *hostname = NULL, *canonhost = NULL, *combined = NULL; ++ char **hrealms = NULL; ++ ++ *princ_out = NULL; ++ ++ assert(iter->princ->length == 2); ++ split_trailer(&iter->princ->data[1], &host, &trailer); ++ ++ hostname = k5memdup0(host.data, host.length, &ret); ++ if (hostname == NULL) ++ goto cleanup; ++ ++ if (iter->princ->type == KRB5_NT_SRV_HST) { ++ /* Expand the hostname with or without DNS as specified. */ ++ ret = expand_hostname(context, hostname, use_dns, &canonhost); ++ if (ret) ++ goto cleanup; ++ } else { ++ canonhost = strdup(hostname); ++ if (canonhost == NULL) { ++ ret = ENOMEM; ++ goto cleanup; ++ } ++ } ++ ++ /* Add the trailer to the expanded hostname. */ ++ if (asprintf(&combined, "%s%.*s", canonhost, ++ trailer.length, trailer.data) < 0) { ++ combined = NULL; ++ ret = ENOMEM; ++ goto cleanup; ++ } ++ ++ /* Don't yield the same host part twice. */ ++ if (iter->canonhost != NULL && strcmp(iter->canonhost, combined) == 0) ++ goto cleanup; ++ ++ free(iter->canonhost); ++ iter->canonhost = combined; ++ combined = NULL; ++ ++ /* If the realm is unknown, look up the realm of the expanded hostname. */ ++ if (iter->princ->realm.length == 0 && !iter->no_hostrealm) { ++ ret = krb5_get_host_realm(context, canonhost, &hrealms); ++ if (ret) ++ goto cleanup; ++ if (hrealms[0] == NULL) { ++ ret = KRB5_ERR_HOST_REALM_UNKNOWN; ++ goto cleanup; ++ } ++ free(iter->realm); ++ if (*hrealms[0] == '\0' && iter->subst_defrealm) { ++ ret = krb5_get_default_realm(context, &iter->realm); ++ if (ret) ++ goto cleanup; ++ } else { ++ iter->realm = strdup(hrealms[0]); ++ if (iter->realm == NULL) { ++ ret = ENOMEM; ++ goto cleanup; ++ } ++ } ++ } ++ ++ iter->copy = *iter->princ; ++ if (iter->realm != NULL) ++ iter->copy.realm = string2data(iter->realm); ++ iter->components[0] = iter->princ->data[0]; ++ iter->components[1] = string2data(iter->canonhost); ++ iter->copy.data = iter->components; ++ *princ_out = &iter->copy; ++ ++cleanup: ++ free(hostname); ++ free(canonhost); ++ free(combined); ++ krb5_free_host_realm(context, hrealms); ++ return ret; ++} ++ ++krb5_error_code ++k5_canonprinc(krb5_context context, struct canonprinc *iter, ++ krb5_const_principal *princ_out) ++{ ++ int step = ++iter->step; ++ ++ *princ_out = NULL; ++ ++ /* If we're not doing fallback, the input principal is canonical. */ ++ if (context->dns_canonicalize_hostname != CANONHOST_FALLBACK || ++ iter->princ->type != KRB5_NT_SRV_HST || iter->princ->length != 2) { ++ *princ_out = (step == 1) ? iter->princ : NULL; ++ return 0; ++ } ++ ++ /* Canonicalize without DNS at step 1, with DNS at step 2. */ ++ if (step > 2) ++ return 0; ++ return canonicalize_princ(context, iter, step == 2, princ_out); + } + + krb5_error_code KRB5_CALLCONV +@@ -185,9 +295,10 @@ krb5_sname_to_principal(krb5_context context, const char *hostname, + { + krb5_error_code ret; + krb5_principal princ; +- const char *realm, *trailer; +- char **hrealms = NULL, *canonhost = NULL, *hostonly = NULL, *concat = NULL; ++ krb5_const_principal cprinc; ++ krb5_boolean use_dns; + char localname[MAXHOSTNAMELEN]; ++ struct canonprinc iter = { NULL }; + + *princ_out = NULL; + +@@ -205,54 +316,26 @@ krb5_sname_to_principal(krb5_context context, const char *hostname, + if (sname == NULL) + sname = "host"; + +- /* If there is a trailer, remove it for now. */ +- trailer = find_trailer(hostname); +- if (trailer != NULL) { +- hostonly = k5memdup0(hostname, trailer - hostname, &ret); +- if (hostonly == NULL) +- goto cleanup; +- hostname = hostonly; +- } +- +- /* Canonicalize the hostname if appropriate. */ +- if (type == KRB5_NT_SRV_HST) { +- ret = krb5_expand_hostname(context, hostname, &canonhost); +- if (ret) +- goto cleanup; +- hostname = canonhost; +- } +- +- /* Find the realm of the host. */ +- ret = krb5_get_host_realm(context, hostname, &hrealms); ++ /* Build an initial principal with what we have. */ ++ ret = krb5_build_principal(context, &princ, 0, KRB5_REFERRAL_REALM, ++ sname, hostname, (char *)NULL); + if (ret) +- goto cleanup; +- if (hrealms[0] == NULL) { +- ret = KRB5_ERR_HOST_REALM_UNKNOWN; +- goto cleanup; +- } +- realm = hrealms[0]; +- +- /* If there was a trailer, put it back on the end. */ +- if (trailer != NULL) { +- if (asprintf(&concat, "%s%s", hostname, trailer) < 0) { +- ret = ENOMEM; +- goto cleanup; +- } +- hostname = concat; +- } +- +- ret = krb5_build_principal(context, &princ, strlen(realm), realm, sname, +- hostname, (char *)NULL); +- if (ret) +- goto cleanup; +- ++ return ret; + princ->type = type; +- *princ_out = princ; + +-cleanup: +- free(hostonly); +- free(canonhost); +- free(concat); +- krb5_free_host_realm(context, hrealms); ++ if (type == KRB5_NT_SRV_HST && ++ context->dns_canonicalize_hostname == CANONHOST_FALLBACK) { ++ /* Delay canonicalization and realm lookup until use. */ ++ *princ_out = princ; ++ return 0; ++ } ++ ++ use_dns = (context->dns_canonicalize_hostname == CANONHOST_TRUE); ++ iter.princ = princ; ++ ret = canonicalize_princ(context, &iter, use_dns, &cprinc); ++ if (!ret) ++ ret = krb5_copy_principal(context, cprinc, princ_out); ++ free_canonprinc(&iter); ++ krb5_free_principal(context, princ); + return ret; + } +diff --git a/src/tests/icred.c b/src/tests/icred.c +index 55f929cd7..d6ce1d5d3 100644 +--- a/src/tests/icred.c ++++ b/src/tests/icred.c +@@ -30,10 +30,7 @@ + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +-/* +- * This program exercises the init_creds APIs in ways kinit doesn't. Right now +- * it is very simplistic, but it can be extended as needed. +- */ ++/* This program exercises the init_creds APIs in ways kinit doesn't. */ + + #include "k5-platform.h" + #include +@@ -56,10 +53,11 @@ check(krb5_error_code code) + int + main(int argc, char **argv) + { +- const char *princstr, *password; ++ const char *ktname = NULL, *sname = NULL, *princstr, *password; + krb5_principal client; + krb5_init_creds_context icc; + krb5_get_init_creds_opt *opt; ++ krb5_keytab keytab = NULL; + krb5_creds creds; + krb5_boolean stepwise = FALSE; + krb5_preauthtype ptypes[64]; +@@ -69,8 +67,11 @@ main(int argc, char **argv) + check(krb5_init_context(&ctx)); + check(krb5_get_init_creds_opt_alloc(ctx, &opt)); + +- while ((c = getopt(argc, argv, "so:X:")) != -1) { ++ while ((c = getopt(argc, argv, "k:so:S:X:")) != -1) { + switch (c) { ++ case 'k': ++ ktname = optarg; ++ break; + case 's': + stepwise = TRUE; + break; +@@ -78,6 +79,9 @@ main(int argc, char **argv) + assert(nptypes < 64); + ptypes[nptypes++] = atoi(optarg); + break; ++ case 'S': ++ sname = optarg; ++ break; + case 'X': + val = strchr(optarg, '='); + if (val != NULL) +@@ -93,12 +97,20 @@ main(int argc, char **argv) + + argc -= optind; + argv += optind; +- if (argc != 2) ++ if (argc != 1 && argc != 2) + abort(); + princstr = argv[0]; + password = argv[1]; + +- check(krb5_parse_name(ctx, princstr, &client)); ++ if (sname != NULL) { ++ check(krb5_sname_to_principal(ctx, princstr, sname, KRB5_NT_SRV_HST, ++ &client)); ++ } else { ++ check(krb5_parse_name(ctx, princstr, &client)); ++ } ++ ++ if (ktname != NULL) ++ check(krb5_kt_resolve(ctx, ktname, &keytab)); + + if (nptypes > 0) + krb5_get_init_creds_opt_set_preauth_list(opt, ptypes, nptypes); +@@ -106,9 +118,16 @@ main(int argc, char **argv) + if (stepwise) { + /* Use the stepwise interface. */ + check(krb5_init_creds_init(ctx, client, NULL, NULL, 0, NULL, &icc)); +- check(krb5_init_creds_set_password(ctx, icc, password)); ++ if (keytab != NULL) ++ check(krb5_init_creds_set_keytab(ctx, icc, keytab)); ++ if (password != NULL) ++ check(krb5_init_creds_set_password(ctx, icc, password)); + check(krb5_init_creds_get(ctx, icc)); + krb5_init_creds_free(ctx, icc); ++ } else if (keytab != NULL) { ++ check(krb5_get_init_creds_keytab(ctx, &creds, client, keytab, 0, NULL, ++ opt)); ++ krb5_free_cred_contents(ctx, &creds); + } else { + /* Use the traditional one-shot interface. */ + check(krb5_get_init_creds_password(ctx, &creds, client, password, NULL, +@@ -116,6 +135,8 @@ main(int argc, char **argv) + krb5_free_cred_contents(ctx, &creds); + } + ++ if (keytab != NULL) ++ krb5_kt_close(ctx, keytab); + krb5_get_init_creds_opt_free(ctx, opt); + krb5_free_principal(ctx, client); + krb5_free_context(ctx); +diff --git a/src/tests/t_sn2princ.py b/src/tests/t_sn2princ.py +index f3e187286..493fba219 100755 +--- a/src/tests/t_sn2princ.py ++++ b/src/tests/t_sn2princ.py +@@ -85,28 +85,9 @@ if offline: + oname = 'ptr-mismatch.kerberos.org' + fname = 'www.kerberos.org' + +-# Test fallback canonicalization krb5_sname_to_principal() results +-# (same as dns_canonicalize_hostname=false). ++# Test fallback canonicalization krb5_sname_to_principal() results. + mark('dns_canonicalize_host=fallback') +-testfc(oname, oname, 'R1') +- +-# Test fallback canonicalization in krb5_get_credentials(). +-oprinc = 'host/' + oname +-fprinc = 'host/' + fname +-shutil.copy(realm.ccache, realm.ccache + '.save') +-realm.addprinc(fprinc) +-# oprinc doesn't exist, so we get the canonicalized fprinc as a fallback. +-msgs = ('Falling back to canonicalized server hostname ' + fname,) +-realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, +- expected_msg=fprinc, expected_trace=msgs) +-realm.addprinc(oprinc) +-# oprinc now exists, but we still get the fprinc ticket from the cache. +-realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, +- expected_msg=fprinc) +-# Without the cached result, we sould get oprinc in preference to fprinc. +-os.rename(realm.ccache + '.save', realm.ccache) +-realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, +- expected_msg=oprinc) ++testfc(oname, oname, '') + + # Verify forward resolution before testing for it. + try: +@@ -118,6 +99,48 @@ if canonname.lower() != fname: + skip_rest('sn2princ tests', + '%s forward resolves to %s, not %s' % (oname, canonname, fname)) + ++# Test fallback canonicalization in krb5_get_credentials(). ++oprinc = 'host/' + oname ++fprinc = 'host/' + fname ++shutil.copy(realm.ccache, realm.ccache + '.save') ++# Test that we only try fprinc once if we enter it as input. ++out, trace = realm.run(['./gcred', 'srv-hst', fprinc + '@'], ++ env=fallback_canon, expected_code=1, return_trace=True) ++msg = 'Requesting tickets for %s@R1, referrals on' % fprinc ++if trace.count(msg) != 1: ++ fail('Expected one try for %s' % fprinc) ++# Create fprinc, and verify that we get it as the canonicalized ++# fallback for oprinc. ++realm.addprinc(fprinc) ++msgs = ('Getting credentials user@R1 -> %s@ using' % oprinc, ++ 'Requesting tickets for %s@R1' % oprinc, ++ 'Requesting tickets for %s@R1' % fprinc, ++ 'Received creds for desired service %s@R1' % fprinc) ++realm.run(['./gcred', 'srv-hst', oprinc + '@'], env=fallback_canon, ++ expected_msg=fprinc, expected_trace=msgs) ++realm.addprinc(oprinc) ++# oprinc now exists, but we still get the fprinc ticket from the cache. ++realm.run(['./gcred', 'srv-hst', oprinc + '@'], env=fallback_canon, ++ expected_msg=fprinc) ++# Without the cached result, we sould get oprinc in preference to fprinc. ++os.rename(realm.ccache + '.save', realm.ccache) ++realm.run(['./gcred', 'srv-hst', oprinc], env=fallback_canon, ++ expected_msg=oprinc) ++ ++# Test fallback canonicalization for krb5_rd_req(). ++realm.run([kadminl, 'ktadd', fprinc]) ++msgs = ('Decrypted AP-REQ with server principal %s@R1' % fprinc, ++ 'AP-REQ ticket: user@R1 -> %s@R1' % fprinc) ++realm.run(['./rdreq', fprinc, oprinc + '@'], env=fallback_canon, ++ expected_trace=msgs) ++ ++# Test fallback canonicalization for getting initial creds with a keytab. ++msgs = ('Getting initial credentials for %s@' % oprinc, ++ 'Found entries for %s@R1 in keytab' % fprinc, ++ 'Retrieving %s@R1 from ' % fprinc) ++realm.run(['./icred', '-k', realm.keytab, '-S', 'host', oname], ++ env=fallback_canon, expected_trace=msgs) ++ + # Test forward-only canonicalization (rdns=false). + mark('rdns=false') + testnr(oname, fname, 'R1') diff --git a/Prevent-deletion-of-K-M.patch b/Prevent-deletion-of-K-M.patch new file mode 100644 index 0000000..5e2c117 --- /dev/null +++ b/Prevent-deletion-of-K-M.patch @@ -0,0 +1,45 @@ +From 7986adf30dffdd16fec43f261a2fa1384e0b8b90 Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Sat, 13 Jun 2020 21:55:54 -0400 +Subject: [PATCH] Prevent deletion of K/M + +In libkadm5srv, do not allow deletion of the master key principal, as +it is very difficult to recover a KDB after doing so. + +ticket: 8913 +(cherry picked from commit 94b936a1bf0a8c67809597c5ea5400d8994d5dd8) +--- + src/lib/kadm5/srv/svr_principal.c | 4 ++++ + src/tests/t_kadmin_acl.py | 6 ++++++ + 2 files changed, 10 insertions(+) + +diff --git a/src/lib/kadm5/srv/svr_principal.c b/src/lib/kadm5/srv/svr_principal.c +index 53ecbe1bc..c2412df31 100644 +--- a/src/lib/kadm5/srv/svr_principal.c ++++ b/src/lib/kadm5/srv/svr_principal.c +@@ -537,6 +537,10 @@ kadm5_delete_principal(void *server_handle, krb5_principal principal) + if (principal == NULL) + return EINVAL; + ++ /* Deleting K/M is mostly unrecoverable, so don't allow it. */ ++ if (krb5_principal_compare(handle->context, principal, master_princ)) ++ return KADM5_PROTECT_PRINCIPAL; ++ + if ((ret = kdb_get_entry(handle, principal, &kdb, &adb))) + return(ret); + ret = k5_kadm5_hook_remove(handle->context, handle->hook_handles, +diff --git a/src/tests/t_kadmin_acl.py b/src/tests/t_kadmin_acl.py +index 86eb59729..8946e8cc4 100755 +--- a/src/tests/t_kadmin_acl.py ++++ b/src/tests/t_kadmin_acl.py +@@ -328,4 +328,10 @@ realm.run([kadmin, '-c', realm.ccache, 'cpw', '-randkey', 'none'], + realm.run([kadmin, '-c', realm.ccache, 'cpw', '-randkey', '-e', 'aes256-cts', + 'none'], expected_code=1, expected_msg=msg) + ++# Test operations disallowed at the libkadm5 layer. ++realm.run([kadminl, 'delprinc', 'K/M'], ++ expected_code=1, expected_msg='Cannot change protected principal') ++realm.run([kadminl, 'cpw', '-pw', 'pw', 'kadmin/history'], ++ expected_code=1, expected_msg='Cannot change protected principal') ++ + success('kadmin ACL enforcement') diff --git a/Refactor-cache-checking-in-TGS-client-code.patch b/Refactor-cache-checking-in-TGS-client-code.patch new file mode 100644 index 0000000..86315ec --- /dev/null +++ b/Refactor-cache-checking-in-TGS-client-code.patch @@ -0,0 +1,188 @@ +From 0cffa5904341460353fa7f99b5aa514fc27a10eb Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Thu, 23 Jul 2020 01:52:43 -0400 +Subject: [PATCH] Refactor cache checking in TGS client code + +(cherry picked from commit 8f2f0a2e8f65c4b39883129967301e3a8986218b) +--- + src/lib/krb5/krb/get_creds.c | 86 +++++++++++++++++++++--------------- + src/lib/krb5/krb/int-proto.h | 6 +-- + src/lib/krb5/krb/s4u_creds.c | 21 +-------- + 3 files changed, 55 insertions(+), 58 deletions(-) + +diff --git a/src/lib/krb5/krb/get_creds.c b/src/lib/krb5/krb/get_creds.c +index b3f01be9b..32401bcb1 100644 +--- a/src/lib/krb5/krb/get_creds.c ++++ b/src/lib/krb5/krb/get_creds.c +@@ -48,10 +48,10 @@ + * and options. The fields of *mcreds will be aliased to the fields + * of in_creds, so the contents of *mcreds should not be freed. + */ +-krb5_error_code +-krb5int_construct_matching_creds(krb5_context context, krb5_flags options, +- krb5_creds *in_creds, krb5_creds *mcreds, +- krb5_flags *fields) ++static krb5_error_code ++construct_matching_creds(krb5_context context, krb5_flags options, ++ krb5_creds *in_creds, krb5_creds *mcreds, ++ krb5_flags *fields) + { + if (!in_creds || !in_creds->server || !in_creds->client) + return EINVAL; +@@ -110,6 +110,50 @@ krb5int_construct_matching_creds(krb5_context context, krb5_flags options, + return 0; + } + ++/* Simple wrapper around krb5_cc_retrieve_cred which allocates the result ++ * container. */ ++static krb5_error_code ++cache_get(krb5_context context, krb5_ccache ccache, krb5_flags flags, ++ krb5_creds *in_creds, krb5_creds **out_creds) ++{ ++ krb5_error_code code; ++ krb5_creds *creds; ++ ++ *out_creds = NULL; ++ ++ creds = malloc(sizeof(*creds)); ++ if (creds == NULL) ++ return ENOMEM; ++ ++ code = krb5_cc_retrieve_cred(context, ccache, flags, in_creds, creds); ++ if (code != 0) { ++ free(creds); ++ return code; ++ } ++ ++ *out_creds = creds; ++ return 0; ++} ++ ++krb5_error_code ++k5_get_cached_cred(krb5_context context, krb5_flags options, ++ krb5_ccache ccache, krb5_creds *in_creds, ++ krb5_creds **creds_out) ++{ ++ krb5_error_code code; ++ krb5_creds mcreds; ++ krb5_flags fields; ++ ++ *creds_out = NULL; ++ ++ code = construct_matching_creds(context, options, in_creds, ++ &mcreds, &fields); ++ if (code) ++ return code; ++ ++ return cache_get(context, ccache, fields, &mcreds, creds_out); ++} ++ + /* + * krb5_tkt_creds_step() is implemented using a tail call style. Every + * begin_*, step_*, or *_request function is responsible for returning an +@@ -235,31 +279,6 @@ cleanup: + return code; + } + +-/* Simple wrapper around krb5_cc_retrieve_cred which allocates the result +- * container. */ +-static krb5_error_code +-cache_get(krb5_context context, krb5_ccache ccache, krb5_flags flags, +- krb5_creds *in_creds, krb5_creds **out_creds) +-{ +- krb5_error_code code; +- krb5_creds *creds; +- +- *out_creds = NULL; +- +- creds = malloc(sizeof(*creds)); +- if (creds == NULL) +- return ENOMEM; +- +- code = krb5_cc_retrieve_cred(context, ccache, flags, in_creds, creds); +- if (code != 0) { +- free(creds); +- return code; +- } +- +- *out_creds = creds; +- return 0; +-} +- + /* + * Set up the request given by ctx->tgs_in_creds, using ctx->cur_tgt. KDC + * options for the requests are determined by ctx->cur_tgt->ticket_flags and +@@ -1023,18 +1042,13 @@ static krb5_error_code + check_cache(krb5_context context, krb5_tkt_creds_context ctx) + { + krb5_error_code code; +- krb5_creds mcreds; +- krb5_flags fields; + krb5_creds req_in_creds; + + /* Check the cache for the originally requested server principal. */ + req_in_creds = *ctx->in_creds; + req_in_creds.server = ctx->req_server; +- code = krb5int_construct_matching_creds(context, ctx->req_options, +- &req_in_creds, &mcreds, &fields); +- if (code) +- return code; +- code = cache_get(context, ctx->ccache, fields, &mcreds, &ctx->reply_creds); ++ code = k5_get_cached_cred(context, ctx->req_options, ctx->ccache, ++ &req_in_creds, &ctx->reply_creds); + if (code == 0) { + ctx->state = STATE_COMPLETE; + return 0; +diff --git a/src/lib/krb5/krb/int-proto.h b/src/lib/krb5/krb/int-proto.h +index fe61bebf5..5211044dc 100644 +--- a/src/lib/krb5/krb/int-proto.h ++++ b/src/lib/krb5/krb/int-proto.h +@@ -79,9 +79,9 @@ clpreauth_otp_initvt(krb5_context context, int maj_ver, int min_ver, + krb5_plugin_vtable vtable); + + krb5_error_code +-krb5int_construct_matching_creds(krb5_context context, krb5_flags options, +- krb5_creds *in_creds, krb5_creds *mcreds, +- krb5_flags *fields); ++k5_get_cached_cred(krb5_context context, krb5_flags options, ++ krb5_ccache ccache, krb5_creds *in_creds, ++ krb5_creds **creds_out); + + #define IS_TGS_PRINC(p) ((p)->length == 2 && \ + data_eq_string((p)->data[0], KRB5_TGS_NAME)) +diff --git a/src/lib/krb5/krb/s4u_creds.c b/src/lib/krb5/krb/s4u_creds.c +index d44f939f8..eadb37cd0 100644 +--- a/src/lib/krb5/krb/s4u_creds.c ++++ b/src/lib/krb5/krb/s4u_creds.c +@@ -1155,29 +1155,12 @@ k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options, + { + krb5_error_code code; + krb5_const_principal canonprinc; +- krb5_creds mcreds, copy, *creds, *ncreds; +- krb5_flags fields; ++ krb5_creds copy, *creds; + struct canonprinc iter = { in_creds->server, .no_hostrealm = TRUE }; + + *out_creds = NULL; + +- code = krb5int_construct_matching_creds(context, options, in_creds, +- &mcreds, &fields); +- if (code != 0) +- return code; +- +- ncreds = calloc(1, sizeof(*ncreds)); +- if (ncreds == NULL) +- return ENOMEM; +- ncreds->magic = KV5M_CRED; +- +- code = krb5_cc_retrieve_cred(context, ccache, fields, &mcreds, ncreds); +- if (code) { +- free(ncreds); +- } else { +- *out_creds = ncreds; +- } +- ++ code = k5_get_cached_cred(context, options, ccache, in_creds, out_creds); + if ((code != KRB5_CC_NOTFOUND && code != KRB5_CC_NOT_KTYPE) || + options & KRB5_GC_CACHED) + return code; diff --git a/Try-kadmin-admin-first-in-libkadm5clnt.patch b/Try-kadmin-admin-first-in-libkadm5clnt.patch new file mode 100644 index 0000000..6249ff5 --- /dev/null +++ b/Try-kadmin-admin-first-in-libkadm5clnt.patch @@ -0,0 +1,159 @@ +From fae915ea7f2734cfd9ef3d5952f25638e675bb7c Mon Sep 17 00:00:00 2001 +From: Greg Hudson +Date: Mon, 27 Jul 2020 01:19:01 -0400 +Subject: [PATCH] Try kadmin/admin first in libkadm5clnt + +The MIT krb5 kadmin protocol originally used kadmin/admin as the +service principal. Commits 493f0da5fbf92b0ac2f10e887706d1964d8a15e8 +and 5cfaec38a8e8f1c4b76228ba0a252987af797ca4 changed it to use +kadmin/hostname preferentially, with kadmin/admin as a fallback, for +interoperability with the Solaris SEAM administrative protocol. + +Change the preference order so that kadmin/admin is tried first, with +kadmin/hostname as a fallback. + +ticket: 8934 (new) +(cherry picked from commit 1d282badfbd6098e3db9d50d22d565c2ec3c8c47) +--- + doc/admin/admin_commands/kadmin_local.rst | 14 ++++++------ + doc/admin/database.rst | 10 ++++----- + src/lib/kadm5/clnt/client_init.c | 26 +++++++++-------------- + src/tests/t_kadmin_acl.py | 14 ++++++++++++ + 4 files changed, 36 insertions(+), 28 deletions(-) + +diff --git a/doc/admin/admin_commands/kadmin_local.rst b/doc/admin/admin_commands/kadmin_local.rst +index fafa61365..33cf3a9cb 100644 +--- a/doc/admin/admin_commands/kadmin_local.rst ++++ b/doc/admin/admin_commands/kadmin_local.rst +@@ -44,9 +44,9 @@ Kerberos principals, password policies, and service key tables + (keytabs). + + The remote kadmin client uses Kerberos to authenticate to kadmind +-using the service principal ``kadmin/ADMINHOST`` (where *ADMINHOST* is +-the fully-qualified hostname of the admin server) or ``kadmin/admin``. +-If the credentials cache contains a ticket for one of these ++using the service principal ``kadmin/admin`` or ``kadmin/ADMINHOST`` ++(where *ADMINHOST* is the fully-qualified hostname of the admin ++server). If the credentials cache contains a ticket for one of these + principals, and the **-c** credentials_cache option is specified, that + ticket is used to authenticate to kadmind. Otherwise, the **-p** and + **-k** options are used to specify the client Kerberos principal name +@@ -100,10 +100,10 @@ OPTIONS + fully anonymous operation. + + **-c** *credentials_cache* +- Use *credentials_cache* as the credentials cache. The +- cache should contain a service ticket for the ``kadmin/ADMINHOST`` +- (where *ADMINHOST* is the fully-qualified hostname of the admin +- server) or ``kadmin/admin`` service; it can be acquired with the ++ Use *credentials_cache* as the credentials cache. The cache ++ should contain a service ticket for the ``kadmin/admin`` or ++ ``kadmin/ADMINHOST`` (where *ADMINHOST* is the fully-qualified ++ hostname of the admin server) service; it can be acquired with the + :ref:`kinit(1)` program. If this option is not specified, kadmin + requests a new service ticket from the KDC, and stores it in its + own temporary ccache. +diff --git a/doc/admin/database.rst b/doc/admin/database.rst +index e62cef7a7..ca19a362a 100644 +--- a/doc/admin/database.rst ++++ b/doc/admin/database.rst +@@ -26,8 +26,8 @@ local filesystem (or through LDAP). kadmin.local is necessary to set + up enough of the database to be able to use the remote version. + + kadmin can authenticate to the admin server using the service +-principal ``kadmin/HOST`` (where *HOST* is the hostname of the admin +-server) or ``kadmin/admin``. If the credentials cache contains a ++principal ``kadmin/admin`` or ``kadmin/HOST`` (where *HOST* is the ++hostname of the admin server). If the credentials cache contains a + ticket for either service principal and the **-c** ccache option is + specified, that ticket is used to authenticate to KADM5. Otherwise, + the **-p** and **-k** options are used to specify the client Kerberos +@@ -811,9 +811,9 @@ Both master and replica sides must have a principal named + ``kiprop/hostname`` (where *hostname* is the lowercase, + fully-qualified, canonical name for the host) registered in the + Kerberos database, and have keys for that principal stored in the +-default keytab file (|keytab|). In release 1.13, the +-``kiprop/hostname`` principal is created automatically for the master +-KDC, but it must still be created for replica KDCs. ++default keytab file (|keytab|). The ``kiprop/hostname`` principal may ++have been created automatically for the master KDC, but it must always ++be created for replica KDCs. + + On the master KDC side, the ``kiprop/hostname`` principal must be + listed in the kadmind ACL file :ref:`kadm5.acl(5)`, and given the +diff --git a/src/lib/kadm5/clnt/client_init.c b/src/lib/kadm5/clnt/client_init.c +index aa08918e2..8d43ab97a 100644 +--- a/src/lib/kadm5/clnt/client_init.c ++++ b/src/lib/kadm5/clnt/client_init.c +@@ -372,22 +372,10 @@ get_init_creds(kadm5_server_handle_t handle, krb5_principal client, + { + kadm5_ret_t code; + krb5_ccache ccache = NULL; +- char svcname[BUFSIZ]; ++ char *svcname, svcbuf[BUFSIZ]; + + *server_out = NULL; + +- /* NULL svcname means use host-based. */ +- if (svcname_in == NULL) { +- code = kadm5_get_admin_service_name(handle->context, +- handle->params.realm, +- svcname, sizeof(svcname)); +- if (code) +- goto error; +- } else { +- strncpy(svcname, svcname_in, sizeof(svcname)); +- svcname[sizeof(svcname)-1] = '\0'; +- } +- + /* + * Acquire a service ticket for svcname@realm for client, using password + * pass (which could be NULL), and create a ccache to store them in. If +@@ -423,13 +411,19 @@ get_init_creds(kadm5_server_handle_t handle, krb5_principal client, + } + handle->lhandle->cache_name = handle->cache_name; + ++ svcname = (svcname_in != NULL) ? svcname_in : KADM5_ADMIN_SERVICE; + code = gic_iter(handle, init_type, ccache, client, pass, svcname, realm, + server_out); + if ((code == KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN + || code == KRB5_CC_NOTFOUND) && svcname_in == NULL) { +- /* Retry with old host-independent service principal. */ +- code = gic_iter(handle, init_type, ccache, client, pass, +- KADM5_ADMIN_SERVICE, realm, server_out); ++ /* Retry with host-based service principal. */ ++ code = kadm5_get_admin_service_name(handle->context, ++ handle->params.realm, ++ svcbuf, sizeof(svcbuf)); ++ if (code) ++ goto error; ++ code = gic_iter(handle, init_type, ccache, client, pass, svcbuf, realm, ++ server_out); + } + /* Improved error messages */ + if (code == KRB5KRB_AP_ERR_BAD_INTEGRITY) code = KADM5_BAD_PASSWORD; +diff --git a/src/tests/t_kadmin_acl.py b/src/tests/t_kadmin_acl.py +index 8946e8cc4..16faf0a9d 100755 +--- a/src/tests/t_kadmin_acl.py ++++ b/src/tests/t_kadmin_acl.py +@@ -328,6 +328,20 @@ realm.run([kadmin, '-c', realm.ccache, 'cpw', '-randkey', 'none'], + realm.run([kadmin, '-c', realm.ccache, 'cpw', '-randkey', '-e', 'aes256-cts', + 'none'], expected_code=1, expected_msg=msg) + ++# Test authentication to kadmin/hostname. ++mark('authentication to kadmin/hostname') ++kadmin_hostname = 'kadmin/' + hostname ++realm.run([kadminl, 'delprinc', 'kadmin/admin']) ++msgs = ('Getting initial credentials for user/admin@KRBTEST.COM', ++ 'Setting initial creds service to kadmin/admin', ++ '/Server not found in Kerberos database', ++ 'Getting initial credentials for user/admin@KRBTEST.COM', ++ 'Setting initial creds service to ' + kadmin_hostname, ++ 'Decrypted AS reply') ++realm.run([kadmin, '-p', 'user/admin', 'listprincs'], expected_code=1, ++ expected_msg="Operation requires ``list'' privilege", ++ input=password('user/admin'), expected_trace=msgs) ++ + # Test operations disallowed at the libkadm5 layer. + realm.run([kadminl, 'delprinc', 'K/M'], + expected_code=1, expected_msg='Cannot change protected principal') diff --git a/krb5.spec b/krb5.spec index 7199f35..081299e 100644 --- a/krb5.spec +++ b/krb5.spec @@ -18,7 +18,7 @@ Summary: The Kerberos network authentication system Name: krb5 Version: 1.18.2 # for prerelease, should be e.g., 0.% {prerelease}.1% { ?dist } (without spaces) -Release: 18%{?dist} +Release: 19%{?dist} # rharwood has trust path to signing key and verifies on check-in Source0: https://web.mit.edu/kerberos/dist/krb5/1.18/krb5-%{version}%{prerelease}.tar.gz @@ -71,6 +71,12 @@ Patch32: Use-two-queues-for-concurrent-t_otp.py-daemons.patch Patch33: Allow-gss_unwrap_iov-of-unpadded-RC4-tokens.patch Patch34: Ignore-bad-enctypes-in-krb5_string_to_keysalts.patch Patch35: Fix-leak-in-KERB_AP_OPTIONS_CBT-server-support.patch +Patch36: Prevent-deletion-of-K-M.patch +Patch37: Try-kadmin-admin-first-in-libkadm5clnt.patch +Patch38: Don-t-create-hostbased-principals-in-new-KDBs.patch +Patch39: Expand-dns_canonicalize_host-fallback-support.patch +Patch40: Cache-S4U2Proxy-requests-by-second-ticket.patch +Patch41: Refactor-cache-checking-in-TGS-client-code.patch License: MIT URL: https://web.mit.edu/kerberos/www/ @@ -632,6 +638,9 @@ exit 0 %{_libdir}/libkadm5srv_mit.so.* %changelog +* Fri Aug 07 2020 Robbie Harwood - 1.18.2-19 +- Expand dns_canonicalize_hostname=fallback support + * Tue Aug 04 2020 Robbie Harwood - 1.18.2-18 - Fix leak in KERB_AP_OPTIONS_CBT server support