/* * MIT Kerberos KDC database backend for FreeIPA * * Authors: Simo Sorce * * Copyright (C) 2011 Simo Sorce, Red Hat * see file 'COPYING' for use and warranty information * * This program is free software you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ipa_kdb.h" #include "ipa_mspac.h" #include #include #include #include "util/time.h" #include "gen_ndr/ndr_krb5pac.h" struct ipadb_adtrusts { char *domain_name; char *flat_name; char *domain_sid; struct dom_sid domsid; struct dom_sid *sid_blacklist_incoming; int len_sid_blacklist_incoming; struct dom_sid *sid_blacklist_outgoing; int len_sid_blacklist_outgoing; struct ipadb_adtrusts *parent; char *parent_name; }; struct ipadb_mspac { char *flat_domain_name; char *flat_server_name; struct dom_sid domsid; char *fallback_group; uint32_t fallback_rid; int num_trusts; struct ipadb_adtrusts *trusts; time_t last_update; }; int krb5_klog_syslog(int, const char *, ...); static char *user_pac_attrs[] = { "objectClass", "uid", "cn", "fqdn", "gidNumber", "krbPrincipalName", "krbCanonicalName", "krbTicketPolicyReference", "krbPrincipalExpiration", "krbPasswordExpiration", "krbPwdPolicyReference", "krbPrincipalType", "krbLastPwdChange", "krbPrincipalAliases", "krbLastSuccessfulAuth", "krbLastFailedAuth", "krbLoginFailedCount", "krbLastAdminUnlock", "krbTicketFlags", "ipaNTSecurityIdentifier", "ipaNTLogonScript", "ipaNTProfilePath", "ipaNTHomeDirectory", "ipaNTHomeDirectoryDrive", NULL }; char *deref_search_attrs[] = { "memberOf", NULL }; static char *memberof_pac_attrs[] = { "gidNumber", "ipaNTSecurityIdentifier", NULL }; static struct { char *service; int length; } supported_services[] = { {"cifs", sizeof("cifs")}, {"HTTP", sizeof("HTTP")}, {NULL, 0} }; #define SID_ID_AUTHS 6 #define SID_SUB_AUTHS 15 #define MAX(a,b) (((a)>(b))?(a):(b)) #define MIN(a,b) (((a)<(b))?(a):(b)) #define AUTHZ_DATA_TYPE_PAC "MS-PAC" #define AUTHZ_DATA_TYPE_PAD "PAD" #define AUTHZ_DATA_TYPE_NONE "NONE" static int string_to_sid(char *str, struct dom_sid *sid) { unsigned long val; char *s, *t; int i; memset(sid, '\0', sizeof(struct dom_sid)); s = str; if (strncasecmp(s, "S-", 2) != 0) { return EINVAL; } s += 2; val = strtoul(s, &t, 10); if (s == t || !t || *t != '-') { return EINVAL; } s = t + 1; sid->sid_rev_num = val; val = strtoul(s, &t, 10); if (s == t || !t) { return EINVAL; } sid->id_auth[2] = (val & 0xff000000) >> 24; sid->id_auth[3] = (val & 0x00ff0000) >> 16; sid->id_auth[4] = (val & 0x0000ff00) >> 8; sid->id_auth[5] = (val & 0x000000ff); for (i = 0; i < SID_SUB_AUTHS; i++) { switch (*t) { case '\0': /* no (more) subauths, we are done with it */ sid->num_auths = i; return 0; case '-': /* there are (more) subauths */ s = t + 1;; break; default: /* garbage */ return EINVAL; } val = strtoul(s, &t, 10); if (s == t || !t) { return EINVAL; } sid->sub_auths[i] = val; } if (*t != '\0') { return EINVAL; } sid->num_auths = i; return 0; } static char *dom_sid_string(TALLOC_CTX *memctx, const struct dom_sid *dom_sid) { size_t c; size_t len; int ofs; uint32_t ia; char *buf; if (dom_sid == NULL) { return NULL; } len = 25 + dom_sid->num_auths * 11; buf = talloc_zero_size(memctx, len); ia = (dom_sid->id_auth[5]) + (dom_sid->id_auth[4] << 8 ) + (dom_sid->id_auth[3] << 16) + (dom_sid->id_auth[2] << 24); ofs = snprintf(buf, len, "S-%u-%lu", (unsigned int) dom_sid->sid_rev_num, (unsigned long) ia); for (c = 0; c < dom_sid->num_auths; c++) { ofs += snprintf(buf + ofs, MAX(len - ofs, 0), "-%lu", (unsigned long) dom_sid->sub_auths[c]); } if (ofs >= len) { talloc_free(buf); return NULL; } return buf; } static struct dom_sid *dom_sid_dup(TALLOC_CTX *memctx, const struct dom_sid *dom_sid) { struct dom_sid *new_sid; size_t c; if (dom_sid == NULL) { return NULL; } new_sid = talloc(memctx, struct dom_sid); if (new_sid == NULL) { return NULL; } new_sid->sid_rev_num = dom_sid->sid_rev_num; for (c = 0; c < SID_ID_AUTHS; c++) { new_sid->id_auth[c] = dom_sid->id_auth[c]; } new_sid->num_auths = dom_sid->num_auths; for (c = 0; c < SID_SUB_AUTHS; c++) { new_sid->sub_auths[c] = dom_sid->sub_auths[c]; } return new_sid; } /* checks if sid1 is a domain of sid2 or compares them exactly if exact_check is true * returns * true -- if sid1 is a domain of sid2 (including full exact match) * false -- otherwise * * dom_sid_check() is supposed to be used with sid1 representing domain SID * and sid2 being either domain or resource SID in the domain */ static bool dom_sid_check(const struct dom_sid *sid1, const struct dom_sid *sid2, bool exact_check) { int c, num; if (sid1 == sid2) { return true; } if (sid1 == NULL) { return false; } if (sid2 == NULL) { return false; } /* If SIDs have different revisions, they are different */ if (sid1->sid_rev_num != sid2->sid_rev_num) return false; /* When number of authorities is different, sids are different * if we were asked to check prefix exactly */ num = sid2->num_auths - sid1->num_auths; if (num != 0) { if (exact_check) { return false; } else { /* otherwise we are dealing with prefix check * and sid2 should have RID compared to the sid1 */ if (num != 1) { return false; } } } /* now either sid1->num_auths == sid2->num_auths or sid1 has no RID */ /* for same size authorities compare them backwards * since RIDs are likely different */ for (c = sid1->num_auths; c >= 0; --c) if (sid1->sub_auths[c] != sid2->sub_auths[c]) return false; /* Finally, compare Identifier authorities */ for (c = 0; c < SID_ID_AUTHS; c++) if (sid1->id_auth[c] != sid2->id_auth[c]) return false; return true; } static bool dom_sid_is_prefix(const struct dom_sid *sid1, const struct dom_sid *sid2) { int c; if (sid1 == sid2) { return true; } if (sid1 == NULL) { return false; } if (sid2 == NULL) { return false; } /* If SIDs have different revisions, they are different */ if (sid1->sid_rev_num != sid2->sid_rev_num) return false; if (sid1->num_auths > sid2->num_auths) return false; /* now sid1->num_auths <= sid2->num_auths */ /* compare up to sid1->num_auth authorities since RIDs are * likely different and we are searching for the prefix */ for (c = 0; c < sid1->num_auths; c++) if (sid1->sub_auths[c] != sid2->sub_auths[c]) return false; /* Finally, compare Identifier authorities */ for (c = 0; c < SID_ID_AUTHS; c++) if (sid1->id_auth[c] != sid2->id_auth[c]) return false; return true; } static int sid_append_rid(struct dom_sid *sid, uint32_t rid) { if (sid->num_auths >= SID_SUB_AUTHS) { return EINVAL; } sid->sub_auths[sid->num_auths++] = rid; return 0; } /** * @brief Takes a user sid and removes the rid. * The sid is changed by this function, * the removed rid is returned too. * * @param sid A user/group SID * @param rid The actual RID found. * * @return 0 on success, EINVAL otherwise. */ static int sid_split_rid(struct dom_sid *sid, uint32_t *rid) { if (sid->num_auths == 0) { return EINVAL; } sid->num_auths--; if (rid != NULL) { *rid = sid->sub_auths[sid->num_auths]; } sid->sub_auths[sid->num_auths] = 0; return 0; } static bool is_master_host(struct ipadb_context *ipactx, const char *fqdn) { int ret; char *master_host_base = NULL; LDAPMessage *result = NULL; krb5_error_code err; ret = asprintf(&master_host_base, "cn=%s,cn=masters,cn=ipa,cn=etc,%s", fqdn, ipactx->base); if (ret == -1) { return false; } err = ipadb_simple_search(ipactx, master_host_base, LDAP_SCOPE_BASE, NULL, NULL, &result); free(master_host_base); ldap_msgfree(result); if (err == 0) { return true; } return false; } static krb5_error_code ipadb_fill_info3(struct ipadb_context *ipactx, LDAPMessage *lentry, TALLOC_CTX *memctx, struct netr_SamInfo3 *info3) { LDAP *lcontext = ipactx->lcontext; LDAPDerefRes *deref_results = NULL; struct dom_sid sid; gid_t prigid = -1; time_t timeres; char *strres; int intres; int ret; int i; char **objectclasses = NULL; size_t c; bool is_host = false; bool is_user = false; bool is_service = false; krb5_principal princ; krb5_data *data; ret = ipadb_ldap_attr_to_strlist(lcontext, lentry, "objectClass", &objectclasses); if (ret == 0 && objectclasses != NULL) { for (c = 0; objectclasses[c] != NULL; c++) { if (strcasecmp(objectclasses[c], "ipaHost") == 0) { is_host = true; } if (strcasecmp(objectclasses[c], "ipaService") == 0) { is_service = true; } if (strcasecmp(objectclasses[c], "ipaNTUserAttrs") == 0) { is_user = true; } free(objectclasses[c]); } } free(objectclasses); if (!is_host && !is_user && !is_service) { /* We only handle users and hosts, and services */ return ENOENT; } if (is_host) { ret = ipadb_ldap_attr_to_str(lcontext, lentry, "fqdn", &strres); if (ret) { /* fqdn is mandatory for hosts */ return ret; } /* Currently we only add a PAC to TGTs for IPA servers to allow SSSD in * ipa_server_mode to access the AD LDAP server */ if (!is_master_host(ipactx, strres)) { free(strres); return ENOENT; } } else if (is_service) { ret = ipadb_ldap_attr_to_str(lcontext, lentry, "krbPrincipalName", &strres); if (ret) { /* krbPrincipalName is mandatory for services */ return ret; } ret = krb5_parse_name(ipactx->kcontext, strres, &princ); free(strres); if (ret) { return ENOENT; } if (krb5_princ_size(ipactx->kcontext, princ) != 2) { krb5_free_principal(ipactx->kcontext, princ); return ENOENT; } data = krb5_princ_component(ipactx->context, princ, 0); for (i = 0; supported_services[i].service; i++) { if (0 == memcmp(data->data, supported_services[i].service, MIN(supported_services[i].length, data->length))) { break; } } if (supported_services[i].service == NULL) { krb5_free_principal(ipactx->kcontext, princ); return ENOENT; } data = krb5_princ_component(ipactx->context, princ, 1); strres = malloc(data->length); if (strres == NULL) { krb5_free_principal(ipactx->kcontext, princ); return ENOENT; } memcpy(strres, data->data, data->length); krb5_free_principal(ipactx->kcontext, princ); /* Only add PAC to TGT to services on IPA masters to allow querying * AD LDAP server */ if (!is_master_host(ipactx, strres)) { free(strres); return ENOENT; } } else { ret = ipadb_ldap_attr_to_str(lcontext, lentry, "uid", &strres); if (ret) { /* uid is mandatory */ return ret; } } info3->base.account_name.string = talloc_strdup(memctx, strres); free(strres); if (is_host || is_service) { prigid = 515; /* Well known RID for domain computers group */ } else { ret = ipadb_ldap_attr_to_int(lcontext, lentry, "gidNumber", &intres); if (ret) { /* gidNumber is mandatory */ return ret; } prigid = intres; } info3->base.logon_time = 0; /* do not have this info yet */ info3->base.logoff_time = -1; /* do not force logoff */ /* TODO: is krbPrinciplaExpiration what we want to use in kickoff_time ? * Needs more investigation */ #if 0 ret = ipadb_ldap_attr_to_time_t(lcontext, lentry, "krbPrincipalExpiration", &timeres); switch (ret) { case 0: unix_to_nt_time(&info3->base.acct_expiry, timeres); break; case ENOENT: info3->base.acct_expiry = -1; break; default: return ret; } #else info3->base.kickoff_time = -1; #endif ret = ipadb_ldap_attr_to_time_t(lcontext, lentry, "krbLastPwdChange", &timeres); switch (ret) { case 0: unix_to_nt_time(&info3->base.last_password_change, timeres); break; case ENOENT: info3->base.last_password_change = 0; break; default: return ret; } /* TODO: from pw policy (ied->pol) */ info3->base.allow_password_change = 0; info3->base.force_password_change = -1; ret = ipadb_ldap_attr_to_str(lcontext, lentry, "cn", &strres); switch (ret) { case 0: info3->base.full_name.string = talloc_strdup(memctx, strres); free(strres); break; case ENOENT: info3->base.full_name.string = ""; break; default: return ret; } ret = ipadb_ldap_attr_to_str(lcontext, lentry, "ipaNTLogonScript", &strres); switch (ret) { case 0: info3->base.logon_script.string = talloc_strdup(memctx, strres); free(strres); break; case ENOENT: info3->base.logon_script.string = ""; break; default: return ret; } ret = ipadb_ldap_attr_to_str(lcontext, lentry, "ipaNTProfilePath", &strres); switch (ret) { case 0: info3->base.profile_path.string = talloc_strdup(memctx, strres); free(strres); break; case ENOENT: info3->base.profile_path.string = ""; break; default: return ret; } ret = ipadb_ldap_attr_to_str(lcontext, lentry, "ipaNTHomeDirectory", &strres); switch (ret) { case 0: info3->base.home_directory.string = talloc_strdup(memctx, strres); free(strres); break; case ENOENT: info3->base.home_directory.string = ""; break; default: return ret; } ret = ipadb_ldap_attr_to_str(lcontext, lentry, "ipaNTHomeDirectoryDrive", &strres); switch (ret) { case 0: info3->base.home_drive.string = talloc_strdup(memctx, strres); free(strres); break; case ENOENT: info3->base.home_drive.string = ""; break; default: return ret; } info3->base.logon_count = 0; /* we do not have this info yet */ info3->base.bad_password_count = 0; /* we do not have this info yet */ if (is_host || is_service) { /* Well know RID of domain controllers group */ info3->base.rid = 516; } else { ret = ipadb_ldap_attr_to_str(lcontext, lentry, "ipaNTSecurityIdentifier", &strres); if (ret) { /* SID is mandatory */ return ret; } ret = string_to_sid(strres, &sid); free(strres); if (ret) { return ret; } ret = sid_split_rid(&sid, &info3->base.rid); if (ret) { return ret; } } ret = ipadb_ldap_deref_results(lcontext, lentry, &deref_results); switch (ret) { LDAPDerefRes *dres; LDAPDerefVal *dval; struct dom_sid gsid; uint32_t trid; gid_t tgid; char *s; int count; case 0: count = 0; for (dres = deref_results; dres; dres = dres->next) { count++; /* count*/ } info3->base.groups.rids = talloc_array(memctx, struct samr_RidWithAttribute, count); if (!info3->base.groups.rids) { ldap_derefresponse_free(deref_results); return ENOMEM; } count = 0; info3->base.primary_gid = 0; for (dres = deref_results; dres; dres = dres->next) { gsid.sid_rev_num = 0; tgid = 0; for (dval = dres->attrVals; dval; dval = dval->next) { if (strcasecmp(dval->type, "gidNumber") == 0) { tgid = strtoul((char *)dval->vals[0].bv_val, &s, 10); if (tgid == 0) { continue; } } if (strcasecmp(dval->type, "ipaNTSecurityIdentifier") == 0) { ret = string_to_sid((char *)dval->vals[0].bv_val, &gsid); if (ret) { continue; } } } if (tgid && gsid.sid_rev_num) { ret = sid_split_rid(&gsid, &trid); if (ret) { continue; } if (tgid == prigid) { info3->base.primary_gid = trid; continue; } info3->base.groups.rids[count].rid = trid; info3->base.groups.rids[count].attributes = SE_GROUP_ENABLED | SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT; count++; } } info3->base.groups.count = count; ldap_derefresponse_free(deref_results); break; case ENOENT: info3->base.groups.count = 0; info3->base.groups.rids = NULL; break; default: return ret; } if (info3->base.primary_gid == 0) { if (is_host || is_service) { info3->base.primary_gid = 515; /* Well known RID for domain computers group */ } else { if (ipactx->mspac->fallback_rid) { info3->base.primary_gid = ipactx->mspac->fallback_rid; } else { /* can't give a pack without a primary group rid */ return ENOENT; } } } /* always zero out, only valid flags are for extra sids with Krb */ info3->base.user_flags = 0; /* netr_UserFlags */ /* always zero out, not used for Krb, only NTLM */ memset(&info3->base.key, '\0', sizeof(info3->base.key)); if (ipactx->mspac->flat_server_name) { info3->base.logon_server.string = talloc_strdup(memctx, ipactx->mspac->flat_server_name); if (!info3->base.logon_server.string) { return ENOMEM; } } else { /* can't give a pack without Server NetBIOS Name :-| */ return ENOENT; } if (ipactx->mspac->flat_domain_name) { info3->base.logon_domain.string = talloc_strdup(memctx, ipactx->mspac->flat_domain_name); if (!info3->base.logon_domain.string) { return ENOMEM; } } else { /* can't give a pack without Domain NetBIOS Name :-| */ return ENOENT; } if (is_host || is_service) { info3->base.domain_sid = talloc_memdup(memctx, &ipactx->mspac->domsid, sizeof(ipactx->mspac->domsid)); } else { /* we got the domain SID for the user sid */ info3->base.domain_sid = talloc_memdup(memctx, &sid, sizeof(sid)); } /* always zero out, not used for Krb, only NTLM */ memset(&info3->base.LMSessKey, '\0', sizeof(info3->base.LMSessKey)); /* TODO: fill based on objectclass, user vs computer, etc... */ info3->base.acct_flags = ACB_NORMAL; /* samr_AcctFlags */ info3->base.sub_auth_status = 0; info3->base.last_successful_logon = 0; info3->base.last_failed_logon = 0; info3->base.failed_logon_count = 0; /* We do not have it */ info3->base.reserved = 0; /* Reserved */ return 0; } static krb5_error_code ipadb_get_pac(krb5_context kcontext, krb5_db_entry *client, krb5_pac *pac) { TALLOC_CTX *tmpctx; struct ipadb_e_data *ied; struct ipadb_context *ipactx; LDAPMessage *results = NULL; LDAPMessage *lentry; DATA_BLOB pac_data; krb5_data data; union PAC_INFO pac_info; krb5_error_code kerr; enum ndr_err_code ndr_err; /* When no client entry is there, we cannot generate MS-PAC */ if (!client) { *pac = NULL; return 0; } ipactx = ipadb_get_context(kcontext); if (!ipactx) { return KRB5_KDB_DBNOTINITED; } ied = (struct ipadb_e_data *)client->e_data; if (ied->magic != IPA_E_DATA_MAGIC) { return EINVAL; } tmpctx = talloc_new(NULL); if (!tmpctx) { return ENOMEM; } memset(&pac_info, 0, sizeof(pac_info)); pac_info.logon_info.info = talloc_zero(tmpctx, struct PAC_LOGON_INFO); if (!pac_info.logon_info.info) { kerr = ENOMEM; goto done; } /* PAC_LOGON_NAME and PAC_TYPE_UPN_DNS_INFO are automatically added * by krb5_pac_sign() later on */ /* == Search PAC info == */ kerr = ipadb_deref_search(ipactx, ied->entry_dn, LDAP_SCOPE_BASE, "(objectclass=*)", user_pac_attrs, deref_search_attrs, memberof_pac_attrs, &results); if (kerr) { goto done; } lentry = ldap_first_entry(ipactx->lcontext, results); if (!lentry) { kerr = ENOENT; goto done; } /* == Fill Info3 == */ kerr = ipadb_fill_info3(ipactx, lentry, tmpctx, &pac_info.logon_info.info->info3); if (kerr) { goto done; } /* == Package PAC == */ ndr_err = ndr_push_union_blob(&pac_data, tmpctx, &pac_info, PAC_TYPE_LOGON_INFO, (ndr_push_flags_fn_t)ndr_push_PAC_INFO); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { kerr = KRB5_KDB_INTERNAL_ERROR; goto done; } kerr = krb5_pac_init(kcontext, pac); if (kerr) { goto done; } data.magic = KV5M_DATA; data.data = (char *)pac_data.data; data.length = pac_data.length; kerr = krb5_pac_add_buffer(kcontext, *pac, KRB5_PAC_LOGON_INFO, &data); done: ldap_msgfree(results); talloc_free(tmpctx); return kerr; } static bool is_cross_realm_krbtgt(krb5_const_principal princ) { if ((princ->length != 2) || (princ->data[0].length != 6) || (strncasecmp(princ->data[0].data, "krbtgt", 6) != 0)) { return false; } if (princ->data[1].length == princ->realm.length && strncasecmp(princ->data[1].data, princ->realm.data, princ->realm.length) == 0) { return false; } return true; } static char *gen_sid_string(TALLOC_CTX *memctx, struct dom_sid *dom_sid, uint32_t rid) { char *str = NULL; int ret; ret = sid_append_rid(dom_sid, rid); if (ret != 0) { krb5_klog_syslog(LOG_ERR, "sid_append_rid failed"); return NULL; } str = dom_sid_string(memctx, dom_sid); ret = sid_split_rid(dom_sid, NULL); if (ret != 0) { krb5_klog_syslog(LOG_ERR, "sid_split_rid failed"); talloc_free(str); return NULL; } return str; } static int get_user_and_group_sids(TALLOC_CTX *memctx, struct PAC_LOGON_INFO_CTR *logon_info, char ***_group_sids) { int ret; size_t c; size_t p = 0; struct dom_sid *domain_sid = NULL; char **group_sids = NULL; domain_sid = dom_sid_dup(memctx, logon_info->info->info3.base.domain_sid); if (domain_sid == NULL) { krb5_klog_syslog(LOG_ERR, "dom_sid_dup failed"); ret = ENOMEM; goto done; } group_sids = talloc_array(memctx, char *, 3 + logon_info->info->info3.base.groups.count + logon_info->info->info3.sidcount); if (group_sids == NULL) { krb5_klog_syslog(LOG_ERR, "talloc_array failed"); ret = ENOMEM; goto done; } group_sids[p] = gen_sid_string(memctx, domain_sid, logon_info->info->info3.base.rid); if (group_sids[p] == NULL) { krb5_klog_syslog(LOG_ERR, "gen_sid_string failed"); ret = EINVAL; goto done; } p++; group_sids[p] = gen_sid_string(memctx, domain_sid, logon_info->info->info3.base.primary_gid); if (group_sids[p] == NULL) { krb5_klog_syslog(LOG_ERR, "gen_sid_string failed"); ret = EINVAL; goto done; } p++; for (c = 0; c < logon_info->info->info3.base.groups.count; c++) { group_sids[p] = gen_sid_string(memctx, domain_sid, logon_info->info->info3.base.groups.rids[c].rid); if (group_sids[p] == NULL) { krb5_klog_syslog(LOG_ERR, "gen_sid_string 2 failed"); ret = EINVAL; goto done; } p++; } for (c = 0; c < logon_info->info->info3.sidcount; c++) { group_sids[p] = dom_sid_string(memctx, logon_info->info->info3.sids[c].sid); if (group_sids[p] == NULL) { krb5_klog_syslog(LOG_ERR, "dom_sid_string failed"); ret = EINVAL; goto done; } p++; } group_sids[p] = NULL; *_group_sids = group_sids; ret = 0; done: talloc_free(domain_sid); if (ret != 0) { talloc_free(group_sids); } return ret; } static int add_groups(TALLOC_CTX *memctx, struct PAC_LOGON_INFO_CTR *logon_info, size_t ipa_group_sids_count, struct dom_sid2 *ipa_group_sids) { size_t c; struct netr_SidAttr *sids = NULL; if (ipa_group_sids_count == 0) { return 0; } sids = talloc_realloc(memctx, logon_info->info->info3.sids, struct netr_SidAttr, logon_info->info->info3.sidcount + ipa_group_sids_count); if (sids == NULL) { return ENOMEM; } for (c = 0; c < ipa_group_sids_count; c++) { sids[c + logon_info->info->info3.sidcount].sid = &ipa_group_sids[c]; sids[c + logon_info->info->info3.sidcount].attributes = SE_GROUP_ENABLED | SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT; } logon_info->info->info3.sidcount += ipa_group_sids_count; logon_info->info->info3.sids = sids; return 0; } static int map_groups(TALLOC_CTX *memctx, krb5_context kcontext, char **group_sids, size_t *_ipa_group_sids_count, struct dom_sid **_ipa_group_sids) { struct ipadb_context *ipactx; krb5_error_code kerr; int ret; LDAPMessage *results = NULL; LDAPMessage *lentry; char *basedn = NULL; char *filter = NULL; LDAPDerefRes *deref_results = NULL; LDAPDerefRes *dres; LDAPDerefVal *dval; size_t c; size_t count = 0; size_t sid_index = 0; struct dom_sid *sids = NULL; char *entry_attrs[] ={"1.1", NULL}; unsigned long gid; struct dom_sid sid; char *endptr; ipactx = ipadb_get_context(kcontext); if (ipactx == NULL) { return KRB5_KDB_DBNOTINITED; } basedn = talloc_asprintf(memctx, "cn=groups,cn=accounts,%s", ipactx->base); if (basedn == NULL) { krb5_klog_syslog(LOG_ERR, "talloc_asprintf failed."); kerr = ENOMEM; goto done; } for (c = 0; group_sids[c] != NULL; c++) { talloc_free(filter); filter = talloc_asprintf(memctx, "(&(objectclass=ipaExternalGroup)(ipaExternalMember=%s))", group_sids[c]); if (filter == NULL) { krb5_klog_syslog(LOG_ERR, "talloc_asprintf failed."); kerr = ENOMEM; goto done; } ldap_msgfree(results); kerr = ipadb_deref_search(ipactx, basedn, LDAP_SCOPE_ONE, filter, entry_attrs, deref_search_attrs, memberof_pac_attrs, &results); if (kerr != 0) { krb5_klog_syslog(LOG_ERR, "ipadb_deref_search failed."); goto done; } lentry = ldap_first_entry(ipactx->lcontext, results); if (lentry == NULL) { continue; } ldap_derefresponse_free(deref_results); ret = ipadb_ldap_deref_results(ipactx->lcontext, lentry, &deref_results); switch (ret) { case ENOENT: /* No entry found, try next SID */ break; case 0: if (deref_results == NULL) { krb5_klog_syslog(LOG_ERR, "No results."); break; } for (dres = deref_results; dres; dres = dres->next) { count++; } sids = talloc_realloc(memctx, sids, struct dom_sid, count); if (sids == NULL) { krb5_klog_syslog(LOG_ERR, "talloc_realloc failed."); kerr = ENOMEM; goto done; } for (dres = deref_results; dres; dres = dres->next) { gid = 0; memset(&sid, '\0', sizeof(struct dom_sid)); for (dval = dres->attrVals; dval; dval = dval->next) { if (strcasecmp(dval->type, "gidNumber") == 0) { errno = 0; gid = strtoul((char *)dval->vals[0].bv_val, &endptr,10); if (gid == 0 || gid >= UINT32_MAX || errno != 0 || *endptr != '\0') { continue; } } if (strcasecmp(dval->type, "ipaNTSecurityIdentifier") == 0) { kerr = string_to_sid((char *)dval->vals[0].bv_val, &sid); if (kerr != 0) { continue; } } } if (gid != 0 && sid.sid_rev_num != 0) { /* TODO: check if gid maps to sid */ if (sid_index >= count) { krb5_klog_syslog(LOG_ERR, "Index larger than " "array, this shoould " "never happen."); kerr = EFAULT; goto done; } memcpy(&sids[sid_index], &sid, sizeof(struct dom_sid)); sid_index++; } } break; default: goto done; } } *_ipa_group_sids_count = sid_index; *_ipa_group_sids = sids; kerr = 0; done: ldap_derefresponse_free(deref_results); talloc_free(basedn); talloc_free(filter); ldap_msgfree(results); return kerr; } static krb5_error_code get_logon_info(krb5_context context, TALLOC_CTX *memctx, krb5_data *pac_blob, struct PAC_LOGON_INFO_CTR *info) { DATA_BLOB pac_data; enum ndr_err_code ndr_err; pac_data.length = pac_blob->length; pac_data.data = (uint8_t *)pac_blob->data; ndr_err = ndr_pull_union_blob(&pac_data, memctx, info, PAC_TYPE_LOGON_INFO, (ndr_pull_flags_fn_t)ndr_pull_PAC_INFO); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { return KRB5_KDB_INTERNAL_ERROR; } return 0; } static krb5_error_code add_local_groups(krb5_context context, TALLOC_CTX *memctx, struct PAC_LOGON_INFO_CTR *info) { int ret; char **group_sids = NULL; size_t ipa_group_sids_count = 0; struct dom_sid *ipa_group_sids = NULL; ret = get_user_and_group_sids(memctx, info, &group_sids); if (ret != 0) { return KRB5_KDB_INTERNAL_ERROR; } ret = map_groups(memctx, context, group_sids, &ipa_group_sids_count, &ipa_group_sids); if (ret != 0) { return KRB5_KDB_INTERNAL_ERROR; } ret = add_groups(memctx, info, ipa_group_sids_count, ipa_group_sids); if (ret != 0) { krb5_klog_syslog(LOG_ERR, "add_groups failed"); return KRB5_KDB_INTERNAL_ERROR; } return 0; } static krb5_error_code save_logon_info(krb5_context context, TALLOC_CTX *memctx, struct PAC_LOGON_INFO_CTR *info, krb5_data *pac_blob) { DATA_BLOB pac_data; enum ndr_err_code ndr_err; ndr_err = ndr_push_union_blob(&pac_data, memctx, info, PAC_TYPE_LOGON_INFO, (ndr_push_flags_fn_t)ndr_push_PAC_INFO); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { return KRB5_KDB_INTERNAL_ERROR; } free(pac_blob->data); pac_blob->data = malloc(pac_data.length); if (pac_blob->data == NULL) { pac_blob->length = 0; return ENOMEM; } memcpy(pac_blob->data, pac_data.data, pac_data.length); pac_blob->length = pac_data.length; return 0; } static struct ipadb_adtrusts *get_domain_from_realm(krb5_context context, krb5_data realm) { struct ipadb_context *ipactx; struct ipadb_adtrusts *domain; int i; ipactx = ipadb_get_context(context); if (!ipactx) { return NULL; } if (ipactx->mspac == NULL) { return NULL; } for (i = 0; i < ipactx->mspac->num_trusts; i++) { domain = &ipactx->mspac->trusts[i]; if (strlen(domain->domain_name) != realm.length) { continue; } if (strncasecmp(domain->domain_name, realm.data, realm.length) == 0) { return domain; } } return NULL; } static struct ipadb_adtrusts *get_domain_from_realm_update(krb5_context context, krb5_data realm) { struct ipadb_context *ipactx; struct ipadb_adtrusts *domain; krb5_error_code kerr; ipactx = ipadb_get_context(context); if (!ipactx) { return NULL; } /* re-init MS-PAC info using default update interval */ kerr = ipadb_reinit_mspac(ipactx, false); if (kerr != 0) { return NULL; } domain = get_domain_from_realm(context, realm); return domain; } static void filter_logon_info_log_message(struct dom_sid *sid) { char *domstr = NULL; domstr = dom_sid_string(NULL, sid); if (domstr) { krb5_klog_syslog(LOG_ERR, "PAC filtering issue: SID [%s] is not allowed " "from a trusted source and will be excluded.", domstr); talloc_free(domstr); } else { krb5_klog_syslog(LOG_ERR, "PAC filtering issue: SID is not allowed " "from a trusted source and will be excluded." "Unable to allocate memory to display SID."); } } static krb5_error_code filter_logon_info(krb5_context context, TALLOC_CTX *memctx, krb5_data realm, struct PAC_LOGON_INFO_CTR *info) { /* We must refuse a PAC that comes signed with a cross realm TGT * where the client pretends to be from a different realm. It is an * attempt at getting us to sign fake credentials with the help of a * compromised trusted realm */ struct ipadb_context *ipactx; struct ipadb_adtrusts *domain; int i, j, k, count; bool result; char *domstr = NULL; domain = get_domain_from_realm_update(context, realm); if (!domain) { return EINVAL; } /* check netbios/flat name */ if (strcasecmp(info->info->info3.base.logon_domain.string, domain->flat_name) != 0) { krb5_klog_syslog(LOG_ERR, "PAC Info mismatch: domain = %s, " "expected flat name = %s, " "found logon name = %s", domain->domain_name, domain->flat_name, info->info->info3.base.logon_domain.string); return EINVAL; } /* check exact sid */ result = dom_sid_check(&domain->domsid, info->info->info3.base.domain_sid, true); if (!result) { domstr = dom_sid_string(NULL, info->info->info3.base.domain_sid); if (!domstr) { return EINVAL; } krb5_klog_syslog(LOG_ERR, "PAC Info mismatch: domain = %s, " "expected domain SID = %s, " "found domain SID = %s", domain->domain_name, domain->domain_sid, domstr); talloc_free(domstr); return EINVAL; } /* Check if this domain has been filtered out by the trust itself*/ if (domain->parent != NULL) { for(k = 0; k < domain->parent->len_sid_blacklist_incoming; k++) { result = dom_sid_check(info->info->info3.base.domain_sid, &domain->parent->sid_blacklist_incoming[k], true); if (result) { filter_logon_info_log_message(info->info->info3.base.domain_sid); return EINVAL; } } } /* According to MS-KILE 25.0, info->info->info3.sids may be non zero, so check * should include different possibilities into account * */ if (info->info->info3.sidcount != 0) { ipactx = ipadb_get_context(context); if (!ipactx || !ipactx->mspac) { return KRB5_KDB_DBNOTINITED; } count = info->info->info3.sidcount; i = 0; j = 0; do { /* Compare SID with our domain without taking RID into account */ result = dom_sid_check(&ipactx->mspac->domsid, info->info->info3.sids[i].sid, false); if (result) { filter_logon_info_log_message(info->info->info3.sids[i].sid); } else { for(k = 0; k < domain->len_sid_blacklist_incoming; k++) { result = dom_sid_is_prefix(&domain->sid_blacklist_incoming[k], info->info->info3.sids[i].sid); if (result) { filter_logon_info_log_message(info->info->info3.sids[i].sid); break; } } } if (result) { j++; memmove(info->info->info3.sids+i, info->info->info3.sids+i+1, count-i-1); } i++; } while (i < count); if (j != 0) { count = count-j; if (count == 0) { /* All SIDs were filtered out */ info->info->info3.sidcount = 0; talloc_free(info->info->info3.sids); info->info->info3.sids = NULL; } else { info->info->info3.sids = talloc_realloc(memctx, info->info->info3.sids, struct netr_SidAttr, count); if (!info->info->info3.sids) { info->info->info3.sidcount = 0; return ENOMEM; } info->info->info3.sidcount = count; } } } /* According to MS-KILE, ResourceGroups must be zero, so check * that it is the case here */ if (info->info->res_group_dom_sid != NULL && info->info->res_groups.count != 0) { return EINVAL; } return 0; } static krb5_error_code ipadb_check_logon_info(krb5_context context, krb5_data origin_realm, krb5_data *pac_blob) { struct PAC_LOGON_INFO_CTR info; krb5_error_code kerr; TALLOC_CTX *tmpctx; tmpctx = talloc_new(NULL); if (!tmpctx) { return ENOMEM; } kerr = get_logon_info(context, tmpctx, pac_blob, &info); if (kerr) { goto done; } kerr = filter_logon_info(context, tmpctx, origin_realm, &info); if (kerr) { goto done; } kerr = add_local_groups(context, tmpctx, &info); if (kerr) { goto done; } kerr = save_logon_info(context, tmpctx, &info, pac_blob); if (kerr) { goto done; } done: talloc_free(tmpctx); return kerr; } static krb5_error_code get_delegation_info(krb5_context context, TALLOC_CTX *memctx, krb5_data *pac_blob, struct PAC_CONSTRAINED_DELEGATION_CTR *info) { DATA_BLOB pac_data; enum ndr_err_code ndr_err; pac_data.length = pac_blob->length; pac_data.data = (uint8_t *)pac_blob->data; ndr_err = ndr_pull_union_blob(&pac_data, memctx, info, PAC_TYPE_CONSTRAINED_DELEGATION, (ndr_pull_flags_fn_t)ndr_pull_PAC_INFO); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { return KRB5_KDB_INTERNAL_ERROR; } return 0; } static krb5_error_code save_delegation_info(krb5_context context, TALLOC_CTX *memctx, struct PAC_CONSTRAINED_DELEGATION_CTR *info, krb5_data *pac_blob) { DATA_BLOB pac_data; enum ndr_err_code ndr_err; ndr_err = ndr_push_union_blob(&pac_data, memctx, info, PAC_TYPE_CONSTRAINED_DELEGATION, (ndr_push_flags_fn_t)ndr_push_PAC_INFO); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { return KRB5_KDB_INTERNAL_ERROR; } free(pac_blob->data); pac_blob->data = malloc(pac_data.length); if (pac_blob->data == NULL) { pac_blob->length = 0; return ENOMEM; } memcpy(pac_blob->data, pac_data.data, pac_data.length); pac_blob->length = pac_data.length; return 0; } static krb5_error_code ipadb_add_transited_service(krb5_context context, krb5_db_entry *proxy, krb5_db_entry *server, krb5_pac old_pac, krb5_pac new_pac) { struct PAC_CONSTRAINED_DELEGATION_CTR info; krb5_data pac_blob = { 0 , 0, NULL }; krb5_error_code kerr; TALLOC_CTX *tmpctx; uint32_t i; char *tmpstr; /* When proxy is NULL, authdata flag on the service principal was cleared * by an admin. We don't generate MS-PAC in this case */ if (proxy == NULL) { return 0; } tmpctx = talloc_new(NULL); if (!tmpctx) { kerr = ENOMEM; goto done; } kerr = krb5_pac_get_buffer(context, old_pac, KRB5_PAC_DELEGATION_INFO, &pac_blob); if (kerr != 0 && kerr != ENOENT) { goto done; } if (pac_blob.length != 0) { kerr = get_delegation_info(context, tmpctx, &pac_blob, &info); if (kerr != 0) { goto done; } } else { info.info = talloc_zero(tmpctx, struct PAC_CONSTRAINED_DELEGATION); if (!info.info) { kerr = ENOMEM; goto done; } } krb5_free_data_contents(context, &pac_blob); memset(&pac_blob, 0, sizeof(krb5_data)); kerr = krb5_unparse_name(context, proxy->princ, &tmpstr); if (kerr != 0) { goto done; } info.info->proxy_target.string = talloc_strdup(tmpctx, tmpstr); krb5_free_unparsed_name(context, tmpstr); if (!info.info->proxy_target.string) { kerr = ENOMEM; goto done; } i = info.info->num_transited_services; info.info->transited_services = talloc_realloc(tmpctx, info.info->transited_services, struct lsa_String, i + 1); if (!info.info->transited_services) { kerr = ENOMEM; goto done; } kerr = krb5_unparse_name(context, server->princ, &tmpstr); if (kerr != 0) { goto done; } info.info->transited_services[i].string = talloc_strdup(tmpctx, tmpstr); krb5_free_unparsed_name(context, tmpstr); if (!info.info->transited_services[i].string) { kerr = ENOMEM; goto done; } info.info->num_transited_services = i + 1; kerr = save_delegation_info(context, tmpctx, &info, &pac_blob); if (kerr != 0) { goto done; } kerr = krb5_pac_add_buffer(context, new_pac, KRB5_PAC_DELEGATION_INFO, &pac_blob); if (kerr) { goto done; } done: krb5_free_data_contents(context, &pac_blob); talloc_free(tmpctx); return kerr; } static krb5_error_code ipadb_verify_pac(krb5_context context, unsigned int flags, krb5_const_principal client_princ, krb5_db_entry *proxy, krb5_db_entry *server, krb5_db_entry *krbtgt, krb5_keyblock *server_key, krb5_keyblock *krbtgt_key, krb5_timestamp authtime, krb5_authdata **authdata, krb5_pac *pac) { krb5_keyblock *srv_key = NULL; krb5_keyblock *priv_key = NULL; krb5_error_code kerr; krb5_ui_4 *types = NULL; size_t num_buffers; krb5_pac old_pac = NULL; krb5_pac new_pac = NULL; krb5_data data; krb5_data pac_blob = { 0 , 0, NULL}; bool is_cross_realm = false; size_t i; kerr = krb5_pac_parse(context, authdata[0]->contents, authdata[0]->length, &old_pac); if (kerr) { goto done; } /* for cross realm trusts cases we need to check the right checksum. * when the PAC is signed by our realm, we can always just check it * passing our realm krbtgt key as the kdc checksum key (privsvr). * But when a trusted realm passes us a PAC the kdc checksum is * generated with that realm krbtgt key, so we need to use the cross * realm krbtgt to check the 'server' checksum instead. */ if (is_cross_realm_krbtgt(krbtgt->princ)) { /* krbtgt from a trusted realm */ is_cross_realm = true; srv_key = krbtgt_key; } else { /* krbtgt from our own realm */ priv_key = krbtgt_key; } kerr = krb5_pac_verify(context, old_pac, authtime, client_princ, srv_key, priv_key); if (kerr) { goto done; } /* Now that the PAC is verified augment it with additional info if * it is coming from a different realm */ if (is_cross_realm) { kerr = krb5_pac_get_buffer(context, old_pac, KRB5_PAC_LOGON_INFO, &pac_blob); if (kerr != 0) { goto done; } kerr = ipadb_check_logon_info(context, client_princ->realm, &pac_blob); if (kerr != 0) { goto done; } } /* extract buffers and rebuilt pac from scratch so that when re-signing * with a different cksum type does not cause issues due to mismatching * signature buffer lengths */ kerr = krb5_pac_init(context, &new_pac); if (kerr) { goto done; } kerr = krb5_pac_get_types(context, old_pac, &num_buffers, &types); if (kerr) { goto done; } for (i = 0; i < num_buffers; i++) { if (types[i] == KRB5_PAC_SERVER_CHECKSUM || types[i] == KRB5_PAC_PRIVSVR_CHECKSUM) { continue; } if (types[i] == KRB5_PAC_LOGON_INFO && pac_blob.length != 0) { kerr = krb5_pac_add_buffer(context, new_pac, types[i], &pac_blob); if (kerr) { krb5_pac_free(context, new_pac); goto done; } continue; } if (types[i] == KRB5_PAC_DELEGATION_INFO && (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION)) { /* skip it here, we will add it explicitly later */ continue; } kerr = krb5_pac_get_buffer(context, old_pac, types[i], &data); if (kerr == 0) { kerr = krb5_pac_add_buffer(context, new_pac, types[i], &data); krb5_free_data_contents(context, &data); } if (kerr) { krb5_pac_free(context, new_pac); goto done; } } if (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) { if (proxy == NULL) { *pac = NULL; kerr = 0; goto done; } kerr = ipadb_add_transited_service(context, proxy, server, old_pac, new_pac); if (kerr) { krb5_pac_free(context, new_pac); goto done; } } *pac = new_pac; done: krb5_free_authdata(context, authdata); krb5_pac_free(context, old_pac); krb5_free_data_contents(context, &pac_blob); free(types); return kerr; } static krb5_error_code ipadb_sign_pac(krb5_context context, krb5_const_principal client_princ, krb5_db_entry *server, krb5_db_entry *krbtgt, krb5_keyblock *server_key, krb5_keyblock *krbtgt_key, krb5_timestamp authtime, krb5_pac pac, krb5_data *pac_data) { krb5_keyblock *right_krbtgt_signing_key = NULL; krb5_key_data *right_krbtgt_key; krb5_db_entry *right_krbtgt = NULL; krb5_principal krbtgt_princ = NULL; krb5_error_code kerr; char *princ = NULL; int ret; /* for cross realm trusts cases we need to sign with the right key. * we need to fetch the right key on our own until the DAL is fixed * to pass us separate check tgt keys and sign tgt keys */ /* We can only ever create the kdc checksum with our realm tgt key. * So, if we get a cross realm tgt we have to fetch our realm tgt * instead. */ if (is_cross_realm_krbtgt(krbtgt->princ)) { ret = asprintf(&princ, "krbtgt/%.*s@%.*s", server->princ->realm.length, server->princ->realm.data, server->princ->realm.length, server->princ->realm.data); if (ret == -1) { princ = NULL; kerr = ENOMEM; goto done; } kerr = krb5_parse_name(context, princ, &krbtgt_princ); if (kerr) { goto done; } kerr = ipadb_get_principal(context, krbtgt_princ, 0, &right_krbtgt); if (kerr) { goto done; } kerr = krb5_dbe_find_enctype(context, right_krbtgt, -1, -1, 0, &right_krbtgt_key); if (kerr) { goto done; } if (!right_krbtgt_key) { kerr = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN; goto done; } right_krbtgt_signing_key = malloc(sizeof(krb5_keyblock)); if (!right_krbtgt_signing_key) { kerr = ENOMEM; goto done; } kerr = krb5_dbe_decrypt_key_data(context, NULL, right_krbtgt_key, right_krbtgt_signing_key, NULL); if (kerr) { goto done; } } else { right_krbtgt_signing_key = krbtgt_key; } kerr = krb5_pac_sign(context, pac, authtime, client_princ, server_key, right_krbtgt_signing_key, pac_data); done: free(princ); krb5_free_principal(context, krbtgt_princ); ipadb_free_principal(context, right_krbtgt); if (right_krbtgt_signing_key != krbtgt_key) { krb5_free_keyblock(context, right_krbtgt_signing_key); } return kerr; } void get_authz_data_types(krb5_context context, krb5_db_entry *entry, bool *_with_pac, bool *_with_pad) { struct ipadb_e_data *ied = NULL; struct ipadb_context *ipactx; size_t c; bool none_found = false; bool srv_none_found = false; char **authz_data_list; bool with_pac = false; bool srv_with_pac = false; bool with_pad = false; bool srv_with_pad = false; char *sep; krb5_data *service_type; char *authz_data_type; bool service_specific; if (entry != NULL) { ied = (struct ipadb_e_data *) entry->e_data; } if (ied == NULL || ied->authz_data == NULL) { if (context == NULL) { krb5_klog_syslog(LOG_ERR, "Missing Kerberos context, no " \ "authorization data will be added."); goto done; } ipactx = ipadb_get_context(context); if (ipactx == NULL || ipactx->authz_data == NULL) { krb5_klog_syslog(LOG_ERR, "No default authorization data types " \ "available, no authorization data will " \ "be added."); goto done; } authz_data_list = ipactx->authz_data; } else { authz_data_list = ied->authz_data; } for (c = 0; authz_data_list[c]; c++) { service_specific = false; authz_data_type = authz_data_list[c]; sep = strchr(authz_data_list[c], ':'); if (sep != NULL) { if (entry->princ == NULL) { krb5_klog_syslog(LOG_ERR, "Missing principal in database " "entry, no authorization data will " \ "be added."); goto done; } service_type = krb5_princ_component(context, entry->princ, 0); if (service_type == NULL) { krb5_klog_syslog(LOG_ERR, "Missing service type in database " "entry, no authorization data will " \ "be added."); goto done; } if (service_type->length == (sep - authz_data_list[c]) && strncmp(authz_data_list[c], service_type->data, service_type->length) == 0) { service_specific = true; authz_data_type = sep + 1; } else { /* Service specific default does not apply, skipping this * entry. */ continue; } } if (strcmp(authz_data_type, AUTHZ_DATA_TYPE_PAC) == 0) { if (service_specific) { srv_with_pac = true; } else { with_pac = true; } } else if (strcmp(authz_data_type, AUTHZ_DATA_TYPE_PAD) == 0) { if (service_specific) { srv_with_pad = true; } else { with_pad = true; } } else if (strcmp(authz_data_type, AUTHZ_DATA_TYPE_NONE) == 0) { if (service_specific) { srv_none_found = true; } else { none_found = true; } } else { krb5_klog_syslog(LOG_ERR, "Ignoring unsupported " \ "authorization data type [%s].", authz_data_list[c]); } } done: if (srv_none_found || srv_with_pac || srv_with_pad) { none_found = srv_none_found; with_pac = srv_with_pac; with_pad = srv_with_pad; } if (none_found) { with_pac = false; with_pad = false; } if (_with_pac != NULL) { *_with_pac = with_pac; } if (_with_pad != NULL) { *_with_pad = with_pad; } } krb5_error_code ipadb_sign_authdata(krb5_context context, unsigned int flags, krb5_const_principal client_princ, krb5_db_entry *client, krb5_db_entry *server, krb5_db_entry *krbtgt, krb5_keyblock *client_key, krb5_keyblock *server_key, krb5_keyblock *krbtgt_key, krb5_keyblock *session_key, krb5_timestamp authtime, krb5_authdata **tgt_auth_data, krb5_authdata ***signed_auth_data) { krb5_const_principal ks_client_princ; krb5_authdata **pac_auth_data = NULL; krb5_authdata *authdata[2] = { NULL, NULL }; krb5_authdata ad; krb5_boolean is_as_req; krb5_error_code kerr; krb5_pac pac = NULL; krb5_data pac_data; struct ipadb_context *ipactx; bool with_pac; bool with_pad; int result; krb5_db_entry *client_entry = NULL; krb5_boolean is_equal; is_as_req = ((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) != 0); /* When using s4u2proxy client_princ actually refers to the proxied user * while client->princ to the proxy service asking for the TGS on behalf * of the proxied user. So always use client_princ in preference */ if (client_princ != NULL) { ks_client_princ = client_princ; if (!is_as_req) { is_equal = false; if ((client != NULL) && (client->princ != NULL)) { is_equal = krb5_principal_compare(context, client_princ, client->princ); } if (!is_equal) { kerr = ipadb_get_principal(context, client_princ, flags, &client_entry); /* If we didn't find client_princ in our database, it might be: * - a principal from another realm, handle it down in ipadb_get/verify_pac() */ if (kerr != 0) { client_entry = NULL; } } } } else { if (client == NULL) { *signed_auth_data = NULL; return 0; } ks_client_princ = client->princ; } /* We only need to check the server entry here, because even if the client * is a service with a valid authorization data it will result to NONE * because ipadb_get_pac() can only generate a pac for 'real' IPA users. * (I assume this will be the same for PAD.) */ get_authz_data_types(context, server, &with_pac, &with_pad); if (with_pad) { krb5_klog_syslog(LOG_ERR, "PAD authorization data is requested but " \ "currently not supported."); } if (is_as_req && with_pac && (flags & KRB5_KDB_FLAG_INCLUDE_PAC)) { /* Be aggressive here: special case for discovering range type * immediately after establishing the trust by IPA framework */ if ((krb5_princ_size(context, ks_client_princ) == 2) && (strncmp(krb5_princ_component(context, ks_client_princ, 0)->data, "HTTP", krb5_princ_component(context, ks_client_princ, 0)->length) == 0)) { ipactx = ipadb_get_context(context); if (!ipactx) { kerr = ENOMEM; goto done; } if (ulc_casecmp(krb5_princ_component(context, ks_client_princ, 1)->data, krb5_princ_component(context, ks_client_princ, 1)->length, ipactx->kdc_hostname, strlen(ipactx->kdc_hostname), NULL, NULL, &result) == 0) { kerr = ipadb_reinit_mspac(ipactx, true); } } kerr = ipadb_get_pac(context, client_entry ? client_entry : client, &pac); if (kerr != 0 && kerr != ENOENT) { goto done; } } if (!is_as_req && with_pac) { /* find the existing PAC, if present */ kerr = krb5_find_authdata(context, tgt_auth_data, NULL, KRB5_AUTHDATA_WIN2K_PAC, &pac_auth_data); if (kerr != 0) { goto done; } /* check or generate pac data */ if ((pac_auth_data == NULL) || (pac_auth_data[0] == NULL)) { if (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) { kerr = ipadb_get_pac(context, client_entry ? client_entry : client, &pac); if (kerr != 0 && kerr != ENOENT) { goto done; } } } else { if (pac_auth_data[1] != NULL) { kerr = KRB5KDC_ERR_BADOPTION; /* FIXME: right error ? */ goto done; } kerr = ipadb_verify_pac(context, flags, ks_client_princ, client, server, krbtgt, server_key, krbtgt_key, authtime, pac_auth_data, &pac); if (kerr != 0) { goto done; } } } if (pac == NULL) { /* No PAC to deal with, proceed */ *signed_auth_data = NULL; kerr = 0; goto done; } kerr = ipadb_sign_pac(context, ks_client_princ, server, krbtgt, server_key, krbtgt_key, authtime, pac, &pac_data); if (kerr != 0) { goto done; } /* put in signed data */ ad.magic = KV5M_AUTHDATA; ad.ad_type = KRB5_AUTHDATA_WIN2K_PAC; ad.contents = (krb5_octet *)pac_data.data; ad.length = pac_data.length; authdata[0] = &ad; kerr = krb5_encode_authdata_container(context, KRB5_AUTHDATA_IF_RELEVANT, authdata, signed_auth_data); krb5_free_data_contents(context, &pac_data); if (kerr != 0) { goto done; } kerr = 0; done: if (client_entry != NULL) { ipadb_free_principal(context, client_entry); } krb5_pac_free(context, pac); return kerr; } static char *get_server_netbios_name(struct ipadb_context *ipactx) { char hostname[MAXHOSTNAMELEN + 1]; /* NOTE: this is 64, too little ? */ char *p; strncpy(hostname, ipactx->kdc_hostname, MAXHOSTNAMELEN); /* May miss termination */ hostname[MAXHOSTNAMELEN] = '\0'; for (p = hostname; *p; p++) { if (*p == '.') { *p = 0; break; } else { *p = toupper(*p); } } return strdup(hostname); } void ipadb_mspac_struct_free(struct ipadb_mspac **mspac) { int i; if (!*mspac) return; free((*mspac)->flat_domain_name); free((*mspac)->flat_server_name); free((*mspac)->fallback_group); if ((*mspac)->num_trusts) { for (i = 0; i < (*mspac)->num_trusts; i++) { free((*mspac)->trusts[i].domain_name); free((*mspac)->trusts[i].flat_name); free((*mspac)->trusts[i].domain_sid); free((*mspac)->trusts[i].sid_blacklist_incoming); free((*mspac)->trusts[i].sid_blacklist_outgoing); free((*mspac)->trusts[i].parent_name); (*mspac)->trusts[i].parent = NULL; } free((*mspac)->trusts); } free(*mspac); *mspac = NULL; } krb5_error_code ipadb_adtrusts_fill_sid_blacklist(char **source_sid_blacklist, struct dom_sid **result_sids, int *result_length) { int len, i; char **source; struct dom_sid *sid_blacklist; if (source_sid_blacklist) { source = source_sid_blacklist; } else { /* Use default hardcoded list */ source = ipa_mspac_well_known_sids; } len = 0; for (i = 0; source && source[i]; i++) { len++; } sid_blacklist = calloc(len, sizeof(struct dom_sid)); if (sid_blacklist == NULL) { return ENOMEM; } for (i = 0; i < len; i++) { (void) string_to_sid(source[i], &sid_blacklist[i]); } *result_sids = sid_blacklist; *result_length = len; return 0; } krb5_error_code ipadb_adtrusts_fill_sid_blacklists(struct ipadb_adtrusts *adtrust, char **sid_blacklist_incoming, char **sid_blacklist_outgoing) { krb5_error_code kerr; kerr = ipadb_adtrusts_fill_sid_blacklist(sid_blacklist_incoming, &adtrust->sid_blacklist_incoming, &adtrust->len_sid_blacklist_incoming); if (kerr) { return kerr; } kerr = ipadb_adtrusts_fill_sid_blacklist(sid_blacklist_outgoing, &adtrust->sid_blacklist_outgoing, &adtrust->len_sid_blacklist_outgoing); if (kerr) { return kerr; } return 0; } krb5_error_code ipadb_mspac_check_trusted_domains(struct ipadb_context *ipactx) { char *attrs[] = { NULL }; char *filter = "(objectclass=ipaNTTrustedDomain)"; char *base = NULL; LDAPMessage *result = NULL; int ret; ret = asprintf(&base, "cn=ad,cn=trusts,%s", ipactx->base); if (ret == -1) { ret = ENOMEM; goto done; } /* Run a quick search if there is any trust defined */ ret = ipadb_simple_search(ipactx, base, LDAP_SCOPE_SUBTREE, filter, attrs, &result); done: ldap_msgfree(result); free(base); return ret; } static void ipadb_free_sid_blacklists(char ***sid_blacklist_incoming, char ***sid_blacklist_outgoing) { int i; if (sid_blacklist_incoming && *sid_blacklist_incoming) { for (i = 0; *sid_blacklist_incoming && (*sid_blacklist_incoming)[i]; i++) { free((*sid_blacklist_incoming)[i]); } free(*sid_blacklist_incoming); *sid_blacklist_incoming = NULL; } if (sid_blacklist_outgoing && *sid_blacklist_outgoing) { for (i = 0; *sid_blacklist_outgoing && (*sid_blacklist_outgoing)[i]; i++) { free((*sid_blacklist_outgoing)[i]); } free(*sid_blacklist_outgoing); *sid_blacklist_outgoing = NULL; } } krb5_error_code ipadb_mspac_get_trusted_domains(struct ipadb_context *ipactx) { struct ipadb_adtrusts *t; LDAP *lc = ipactx->lcontext; char *attrs[] = { "cn", "ipaNTTrustPartner", "ipaNTFlatName", "ipaNTTrustedDomainSID", "ipaNTSIDBlacklistIncoming", "ipaNTSIDBlacklistOutgoing", NULL }; char *filter = "(objectclass=ipaNTTrustedDomain)"; krb5_error_code kerr; LDAPMessage *res = NULL; LDAPMessage *le; LDAPRDN rdn; char *base = NULL; char *dnstr = NULL; char *dnl = NULL; char **sid_blacklist_incoming = NULL; char **sid_blacklist_outgoing = NULL; int ret, n, i; ret = asprintf(&base, "cn=ad,cn=trusts,%s", ipactx->base); if (ret == -1) { ret = ENOMEM; goto done; } kerr = ipadb_simple_search(ipactx, base, LDAP_SCOPE_SUBTREE, filter, attrs, &res); if (kerr == KRB5_KDB_NOENTRY) { /* nothing to do, there are no trusts */ ret = 0; goto done; } else if (kerr != 0) { ret = EIO; goto done; } for (le = ldap_first_entry(lc, res); le; le = ldap_next_entry(lc, le)) { dnstr = ldap_get_dn(lc, le); if (dnstr == NULL) { ret = ENOMEM; goto done; } n = ipactx->mspac->num_trusts; ipactx->mspac->num_trusts++; t = realloc(ipactx->mspac->trusts, sizeof(struct ipadb_adtrusts) * ipactx->mspac->num_trusts); if (!t) { ret = ENOMEM; goto done; } ipactx->mspac->trusts = t; memset(&t[n], 0, sizeof(t[n])); ret = ipadb_ldap_attr_to_str(lc, le, "cn", &t[n].domain_name); if (ret) { ret = EINVAL; goto done; } ret = ipadb_ldap_attr_to_str(lc, le, "ipaNTFlatName", &t[n].flat_name); if (ret) { ret = EINVAL; goto done; } ret = ipadb_ldap_attr_to_str(lc, le, "ipaNTTrustedDomainSID", &t[n].domain_sid); if (ret) { ret = EINVAL; goto done; } ret = string_to_sid(t[n].domain_sid, &t[n].domsid); if (ret) { ret = EINVAL; goto done; } ret = ipadb_ldap_attr_to_strlist(lc, le, "ipaNTSIDBlacklistIncoming", &sid_blacklist_incoming); if (ret) { if (ret == ENOENT) { /* This attribute is optional */ ret = 0; sid_blacklist_incoming = NULL; } else { ret = EINVAL; goto done; } } ret = ipadb_ldap_attr_to_strlist(lc, le, "ipaNTSIDBlacklistOutgoing", &sid_blacklist_outgoing); if (ret) { if (ret == ENOENT) { /* This attribute is optional */ ret = 0; sid_blacklist_outgoing = NULL; } else { ret = EINVAL; goto done; } } ret = ipadb_adtrusts_fill_sid_blacklists(&t[n], sid_blacklist_incoming, sid_blacklist_outgoing); if (ret) { goto done; } ipadb_free_sid_blacklists(&sid_blacklist_incoming, &sid_blacklist_outgoing); /* Parse first two RDNs of the entry to find its parent */ dnl = strcasestr(dnstr, base); if (dnl == NULL) { goto done; } /* Note that after ldap_str2rdn() call dnl will point to end of one RDN * which would be '\0' for trust root domain and ',' for subdomain */ dnl--; dnl[0] = '\0'; ret = ldap_str2rdn(dnstr, &rdn, &dnl, LDAP_DN_FORMAT_LDAPV3); if (ret) { goto done; } ldap_rdnfree(rdn); if (dnl[0] != '\0') { dnl++; ret = ldap_str2rdn(dnl, &rdn, &dnl, LDAP_DN_FORMAT_LDAPV3); if (ret) { goto done; } t[n].parent_name = strndup(rdn[0]->la_value.bv_val, rdn[0]->la_value.bv_len); ldap_rdnfree(rdn); } free(dnstr); dnstr = NULL; } /* Traverse through all trusts and resolve parents */ t = ipactx->mspac->trusts; for (i = 0; i < ipactx->mspac->num_trusts; i++) { if (t[i].parent_name != NULL) { for (n = 0; n < ipactx->mspac->num_trusts; n++) { if (strcasecmp(t[i].parent_name, t[n].domain_name) == 0) { t[i].parent = &t[n]; } } } } ret = 0; done: if (ret != 0) { krb5_klog_syslog(LOG_ERR, "Failed to read list of trusted domains"); } free(dnstr); free(base); ipadb_free_sid_blacklists(&sid_blacklist_incoming, &sid_blacklist_outgoing); ldap_msgfree(res); return ret; } krb5_error_code ipadb_reinit_mspac(struct ipadb_context *ipactx, bool force_reinit) { char *dom_attrs[] = { "ipaNTFlatName", "ipaNTFallbackPrimaryGroup", "ipaNTSecurityIdentifier", NULL }; char *grp_attrs[] = { "ipaNTSecurityIdentifier", NULL }; krb5_error_code kerr; LDAPMessage *result = NULL; LDAPMessage *lentry; struct dom_sid gsid; char *resstr; int ret; time_t now; /* Do not update the mspac struct more than once a minute. This would * avoid heavy load on the directory server if there are lots of requests * from domains which we do not trust. */ now = time(NULL); if (ipactx->mspac != NULL && (force_reinit == false) && (now > ipactx->mspac->last_update) && (now - ipactx->mspac->last_update) < 60) { return 0; } if (ipactx->mspac && ipactx->mspac->num_trusts == 0) { /* Check if there is any trust configured. If not, just return * and do not re-initialize the MS-PAC structure. */ kerr = ipadb_mspac_check_trusted_domains(ipactx); if (kerr == KRB5_KDB_NOENTRY) { kerr = 0; goto done; } else if (kerr != 0) { goto done; } } /* clean up in case we had old values around */ ipadb_mspac_struct_free(&ipactx->mspac); ipactx->mspac = calloc(1, sizeof(struct ipadb_mspac)); if (!ipactx->mspac) { kerr = ENOMEM; goto done; } ipactx->mspac->last_update = now; kerr = ipadb_simple_search(ipactx, ipactx->base, LDAP_SCOPE_SUBTREE, "(objectclass=ipaNTDomainAttrs)", dom_attrs, &result); if (kerr == KRB5_KDB_NOENTRY) { return ENOENT; } else if (kerr != 0) { return EIO; } lentry = ldap_first_entry(ipactx->lcontext, result); if (!lentry) { kerr = ENOENT; goto done; } ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "ipaNTFlatName", &ipactx->mspac->flat_domain_name); if (ret) { kerr = ret; goto done; } ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "ipaNTSecurityIdentifier", &resstr); if (ret) { kerr = ret; goto done; } ret = string_to_sid(resstr, &ipactx->mspac->domsid); if (ret) { kerr = ret; free(resstr); goto done; } free(resstr); free(ipactx->mspac->flat_server_name); ipactx->mspac->flat_server_name = get_server_netbios_name(ipactx); if (!ipactx->mspac->flat_server_name) { kerr = ENOMEM; goto done; } ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "ipaNTFallbackPrimaryGroup", &ipactx->mspac->fallback_group); if (ret && ret != ENOENT) { kerr = ret; goto done; } /* result and lentry not valid any more from here on */ ldap_msgfree(result); result = NULL; lentry = NULL; if (ret != ENOENT) { kerr = ipadb_simple_search(ipactx, ipactx->mspac->fallback_group, LDAP_SCOPE_BASE, "(objectclass=posixGroup)", grp_attrs, &result); if (kerr && kerr != KRB5_KDB_NOENTRY) { kerr = ret; goto done; } lentry = ldap_first_entry(ipactx->lcontext, result); if (!lentry) { kerr = ENOENT; goto done; } if (kerr == 0) { ret = ipadb_ldap_attr_to_str(ipactx->lcontext, lentry, "ipaNTSecurityIdentifier", &resstr); if (ret && ret != ENOENT) { kerr = ret; goto done; } if (ret == 0) { ret = string_to_sid(resstr, &gsid); if (ret) { free(resstr); kerr = ret; goto done; } ret = sid_split_rid(&gsid, &ipactx->mspac->fallback_rid); if (ret) { free(resstr); kerr = ret; goto done; } free(resstr); } } } kerr = ipadb_mspac_get_trusted_domains(ipactx); done: ldap_msgfree(result); return kerr; } krb5_error_code ipadb_check_transited_realms(krb5_context kcontext, const krb5_data *tr_contents, const krb5_data *client_realm, const krb5_data *server_realm) { struct ipadb_context *ipactx; bool has_transited_contents, has_client_realm, has_server_realm; int i; krb5_error_code ret; ipactx = ipadb_get_context(kcontext); if (!ipactx || !ipactx->mspac) { return KRB5_KDB_DBNOTINITED; } has_transited_contents = false; has_client_realm = false; has_server_realm = false; /* First, compare client or server realm with ours */ if (strncasecmp(client_realm->data, ipactx->realm, client_realm->length) == 0) { has_client_realm = true; } if (strncasecmp(server_realm->data, ipactx->realm, server_realm->length) == 0) { has_server_realm = true; } if ((tr_contents->length == 0) || (tr_contents->data[0] == '\0')) { /* For in-realm case allow transition */ if (has_client_realm && has_server_realm) { return 0; } /* Since transited realm is empty, we don't need to check for it, it is a direct trust case */ has_transited_contents = true; } if (!ipactx->mspac || !ipactx->mspac->trusts) { return KRB5_PLUGIN_NO_HANDLE; } /* Iterate through list of trusts and check if any of input belongs to any of the trust */ for(i=0; i < ipactx->mspac->num_trusts ; i++) { if (!has_transited_contents && (strncasecmp(tr_contents->data, ipactx->mspac->trusts[i].domain_name, tr_contents->length) == 0)) { has_transited_contents = true; } if (!has_client_realm && (strncasecmp(client_realm->data, ipactx->mspac->trusts[i].domain_name, client_realm->length) == 0)) { has_client_realm = true; } if (!has_server_realm && (strncasecmp(server_realm->data, ipactx->mspac->trusts[i].domain_name, server_realm->length) == 0)) { has_server_realm = true; } } ret = KRB5KRB_AP_ERR_ILL_CR_TKT; if (has_client_realm && has_transited_contents && has_server_realm) { ret = 0; } return ret; }