/* * Copyright (c) 2005, PADL Software Pty Ltd. * All rights reserved. * * Portions Copyright (c) 2009 - 2011 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. Neither the name of PADL Software nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY PADL SOFTWARE AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL PADL SOFTWARE OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include "kcm_locl.h" #ifdef HAVE_NOTIFY_H #include #include #endif #include #include #include #include static void kcm_release_ccache_locked(krb5_context, kcm_ccache); HEIMDAL_MUTEX ccache_mutex = HEIMDAL_MUTEX_INITIALIZER; TAILQ_HEAD(ccache_head, kcm_ccache_data); static struct ccache_head ccache_head = TAILQ_HEAD_INITIALIZER(ccache_head); static uint32_t ccache_nextid = 0; #ifdef HAVE_NOTIFY_H static uint64_t relative_nano_time(void) { static uint64_t factor; uint64_t now; now = mach_absolute_time(); if (factor == 0) { mach_timebase_info_data_t base; (void)mach_timebase_info(&base); factor = base.numer / base.denom; } return now * factor; } #endif /* * Deliver a notification every NOTIFY_TIME_LIMIT */ static void notify_changed_caches(void) { #ifdef HAVE_NOTIFY_H static uint64_t last_change; static int notify_pending; #define NOTIFY_TIME_LIMIT (NSEC_PER_SEC / 2) dispatch_async(dispatch_get_main_queue(), ^{ uint64_t now, diff; if (notify_pending) return; now = relative_nano_time(); if (now < last_change) diff = NOTIFY_TIME_LIMIT; else diff = now - last_change; if (diff >= NOTIFY_TIME_LIMIT) { notify_post(KRB5_KCM_NOTIFY_CACHE_CHANGED); last_change = now; } else { notify_pending = 1; /* wait up to deliver the event */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NOTIFY_TIME_LIMIT - diff), dispatch_get_main_queue(), ^{ notify_pending = 0; last_change = relative_nano_time(); notify_post(KRB5_KCM_NOTIFY_CACHE_CHANGED); }); } }); #endif } char * kcm_ccache_nextid(pid_t pid, uid_t uid) { kcm_ccache c; char *name = NULL; unsigned n; HEIMDAL_MUTEX_lock(&ccache_mutex); while (name == NULL) { n = ++ccache_nextid; asprintf(&name, "%ld:%u", (long)uid, n); /* find dups */ TAILQ_FOREACH(c, &ccache_head, members) { if (strcmp(c->name, name) == 0) { free(name); name = NULL; break; } } } HEIMDAL_MUTEX_unlock(&ccache_mutex); return name; } krb5_error_code kcm_ccache_resolve_by_name(krb5_context context, const char *name, kcm_ccache *ccache) { kcm_ccache p; krb5_error_code ret; *ccache = NULL; ret = KRB5_FCC_NOFILE; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { if (strcmp(p->name, name) == 0) { ret = 0; break; } } if (ret == 0) { kcm_retain_ccache(context, p); *ccache = p; } HEIMDAL_MUTEX_unlock(&ccache_mutex); return ret; } krb5_error_code kcm_ccache_resolve_by_uuid(krb5_context context, kcmuuid_t uuid, kcm_ccache *ccache) { kcm_ccache p; krb5_error_code ret; *ccache = NULL; ret = KRB5_FCC_NOFILE; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { if (memcmp(p->uuid, uuid, sizeof(kcmuuid_t)) == 0) { ret = 0; break; } } if (ret == 0) { kcm_retain_ccache(context, p); *ccache = p; } HEIMDAL_MUTEX_unlock(&ccache_mutex); return ret; } krb5_error_code kcm_ccache_get_uuids(krb5_context context, kcm_client *client, kcm_operation opcode, krb5_storage *sp) { kcm_ccache p; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { krb5_error_code ret; ret = kcm_access(context, client, opcode, p); if (ret) continue; krb5_storage_write(sp, p->uuid, sizeof(p->uuid)); } HEIMDAL_MUTEX_unlock(&ccache_mutex); return 0; } krb5_error_code kcm_debug_ccache(krb5_context context) { kcm_ccache p; TAILQ_FOREACH(p, &ccache_head, members) { char *cpn = NULL, *spn = NULL; int ncreds = 0; struct kcm_creds *k; KCM_ASSERT_VALID(p); for (k = p->creds; k != NULL; k = k->next) ncreds++; if (p->client != NULL) krb5_unparse_name(context, p->client, &cpn); if (p->server != NULL) krb5_unparse_name(context, p->server, &spn); kcm_log(7, "cache name %s refcnt %d flags %04x" "uid %d client %s server %s ncreds %d", p->name, p->refcnt, p->flags, p->uid, (cpn == NULL) ? "" : cpn, (spn == NULL) ? "" : spn, ncreds); if (cpn != NULL) free(cpn); if (spn != NULL) free(spn); } return 0; } static void kcm_free_ccache_data_internal(krb5_context context, kcm_ccache cache) { KCM_ASSERT_VALID(cache); if (cache->name != NULL) { free(cache->name); cache->name = NULL; } if (cache->flags & KCM_FLAGS_USE_KEYTAB) { krb5_kt_close(context, cache->keytab); cache->keytab = NULL; } else if (cache->flags & KCM_FLAGS_USE_PASSWORD) { memset(cache->password, 0, strlen(cache->password)); free(cache->password); } cache->flags = 0; cache->uid = -1; cache->session = -1; kcm_zero_ccache_data_internal(context, cache); cache->tkt_life = 0; cache->renew_life = 0; cache->refcnt = 0; } krb5_error_code kcm_ccache_destroy(krb5_context context, const char *name) { kcm_ccache p; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { if (strcmp(p->name, name) == 0) { HEIMDAL_MUTEX_lock(&p->mutex); TAILQ_REMOVE(&ccache_head, p, members); break; } } HEIMDAL_MUTEX_unlock(&ccache_mutex); if (p == NULL) return KRB5_FCC_NOFILE; notify_changed_caches(); /* XXX blocking */ heim_ipc_event_cancel(p->renew_event); heim_ipc_event_free(p->renew_event); p->renew_event = NULL; heim_ipc_event_cancel(p->expire_event); heim_ipc_event_free(p->expire_event); p->expire_event = NULL; kcm_release_ccache_locked(context, p); return 0; } #define KCM_EVENT_QUEUE_INTERVAL 60 void kcm_update_renew_time(kcm_ccache ccache) { time_t renewtime = time(NULL) + 3600 * 2; time_t expire = ccache->expire; /* if the ticket is about to expire in less then QUEUE_INTERVAL, * don't bother */ if (time(NULL) + KCM_EVENT_QUEUE_INTERVAL > expire) return; if (renewtime > expire - KCM_EVENT_QUEUE_INTERVAL) renewtime = expire - KCM_EVENT_QUEUE_INTERVAL; kcm_log(1, "%s: will try to renew credentals in %d seconds", ccache->name, (int)(renewtime - time(NULL))); heim_ipc_event_set_time(ccache->renew_event, renewtime); ccache->renew_time = renewtime; } void kcm_update_expire_time(kcm_ccache cache, time_t t) { if (t == 0) { t = time(NULL); } else if (t < time(NULL)) { cache->next_refresh_time = 0; return; } cache->next_refresh_time = t; heim_ipc_event_set_time(cache->expire_event, t); } static void renew_func(heim_event_t event, void *ptr) { kcm_ccache cache = ptr; krb5_error_code ret; time_t expire; kcm_log(0, "cache: %s renewing", cache->name); HEIMDAL_MUTEX_lock(&cache->mutex); if (cache->flags & KCM_MASK_KEY_PRESENT) { ret = kcm_ccache_acquire(kcm_context, cache, &expire); krb5_warn(kcm_context, ret, "cache: %s acquire complete", cache->name); } else { ret = kcm_ccache_refresh(kcm_context, cache, &expire); krb5_warn(kcm_context, ret, "cache: %s renew complete", cache->name); } switch (ret) { case KRB5KRB_AP_ERR_BAD_INTEGRITY: case KRB5KRB_AP_ERR_MODIFIED: case KRB5KDC_ERR_PREAUTH_FAILED: /* bad password, drop it like dead */ kcm_log(0, "cache: %s got bad password, stop renewing", cache->name); kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_STOPPED, ret); cache->flags &= ~KCM_MASK_KEY_PRESENT; break; case 0: kcm_data_changed = 1; cache->expire = expire; kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_SUCCESS, 0); notify_changed_caches(); break; default: { const char *msg = krb5_get_error_message(kcm_context, ret); kcm_log(0, "failed to renew: %s: %d", msg, ret); krb5_free_error_message(kcm_context, msg); kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_FAILED, ret); break; } } kcm_update_renew_time(cache); HEIMDAL_MUTEX_unlock(&cache->mutex); } static void expire_func(heim_event_t event, void *ptr) { kcm_ccache cache = ptr; krb5_error_code ret; kcm_log(0, "cache: %s expired", cache->name); HEIMDAL_MUTEX_lock(&cache->mutex); heim_ipc_event_cancel(cache->renew_event); cache->next_refresh_time = 0; if (cache->flags & KCM_MASK_KEY_PRESENT){ time_t expire; ret = kcm_ccache_acquire(kcm_context, cache, &expire); switch (ret) { case KRB5KRB_AP_ERR_BAD_INTEGRITY: case KRB5KRB_AP_ERR_MODIFIED: case KRB5KDC_ERR_PREAUTH_FAILED: /* bad password, drop it like dead */ kcm_log(0, "cache: %s got bad password, stop renewing", cache->name); kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_STOPPED, ret); cache->flags &= ~KCM_MASK_KEY_PRESENT; break; case 0: kcm_data_changed = 1; kcm_log(0, "cache: %s got new tickets (expire in %d seconds)", cache->name, (int)(expire - time(NULL))); cache->expire = expire; kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_SUCCESS, 0); notify_changed_caches(); break; default: kcm_data_changed = 1; kcm_update_expire_time(cache, time(NULL) + 300); kcm_ccache_update_acquire_status(kcm_context, cache, KCM_STATUS_ACQUIRE_FAILED, ret); notify_changed_caches(); break; } } else { kcm_log(0, "cache: %s expired", cache->name); notify_changed_caches(); } HEIMDAL_MUTEX_unlock(&cache->mutex); } static void release_cache(void *ctx) { kcm_release_ccache(kcm_context, (kcm_ccache)ctx); } static krb5_error_code kcm_ccache_alloc(krb5_context context, const char *name, kcm_ccache *cache) { kcm_ccache p = NULL; krb5_error_code ret; /* First, check for duplicates */ HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { if (strcmp(p->name, name) == 0) { ret = KRB5_CC_WRITE; goto out; } } /* * Create an enpty cache for us. */ p = calloc(1, sizeof(*p)); if (p == NULL) { ret = KRB5_CC_NOMEM; goto out; } HEIMDAL_MUTEX_init(&p->mutex); CCRandomCopyBytes(kCCRandomDefault, p->uuid, sizeof(p->uuid)); p->name = strdup(name); if (p->name == NULL) { ret = KRB5_CC_NOMEM; goto out; } p->refcnt = 3; /* on members, and both events */ p->holdcount = 1; p->flags = 0; p->uid = -1; p->client = NULL; p->server = NULL; p->creds = NULL; p->keytab = NULL; p->password = NULL; p->tkt_life = 0; p->renew_life = 0; p->renew_event = heim_ipc_event_create_f(renew_func, p); p->expire_event = heim_ipc_event_create_f(expire_func, p); heim_ipc_event_set_final_f(p->renew_event, release_cache); heim_ipc_event_set_final_f(p->expire_event, release_cache); TAILQ_INSERT_HEAD(&ccache_head, p, members); *cache = p; HEIMDAL_MUTEX_unlock(&ccache_mutex); return 0; out: HEIMDAL_MUTEX_unlock(&ccache_mutex); if (p != NULL) { HEIMDAL_MUTEX_destroy(&p->mutex); free(p); } *cache = NULL; return ret; } #define KRB5_CONF_NAME "krb5_ccache_conf_data" #define KRB5_REALM_NAME "X-CACHECONF:" krb5_error_code kcm_ccache_update_acquire_status(krb5_context context, kcm_ccache ccache, int status, krb5_error_code ret) { krb5_creds cred; uint8_t st[12]; uint32_t u32; if ((ccache->flags & KCM_MASK_KEY_PRESENT) == 0) return 0; if (ccache->client == NULL) return 0; switch (status) { case KCM_STATUS_ACQUIRE_START: kcm_update_expire_time(ccache, 0); break; case KCM_STATUS_ACQUIRE_STOPPED: ccache->next_refresh_time = 0; break; case KCM_STATUS_ACQUIRE_FAILED: kcm_update_expire_time(ccache, time(NULL) + 300); break; case KCM_STATUS_ACQUIRE_SUCCESS: { time_t next_refresh, now = time(NULL); if (ccache->expire > now) { next_refresh = ccache->expire; /* try to acquire credential just before */ if (ccache->expire - now > 300) next_refresh -= 300; kcm_update_expire_time(ccache, next_refresh + 300); } else { ccache->next_refresh_time = 0; } break; } default: heim_assert(0, "invalid status"); break; } memcpy(st, "krb5", 4); u32 = htonl(status); memcpy(&st[4], &u32, sizeof(u32)); u32 = htonl(ret); memcpy(&st[8], &u32, sizeof(u32)); memset(&cred, 0, sizeof(cred)); cred.client = ccache->client; ret = krb5_make_principal(context, &cred.server, KRB5_REALM_NAME, KRB5_CONF_NAME, KCM_STATUS_KEY, NULL); if (ret) return ret; cred.ticket.data = st; cred.ticket.length = sizeof(st); cred.times.authtime = time(NULL); cred.times.endtime = cred.times.authtime + 3600 * 24 * 30; ret = kcm_ccache_store_cred_internal(context, ccache, &cred, NULL, 1); krb5_free_principal(context, cred.server); return ret; } krb5_error_code kcm_ccache_enqueue_default(krb5_context context, kcm_ccache cache, krb5_creds *newcred) { if (newcred == NULL) { } else if (cache->flags & KCM_MASK_KEY_PRESENT) { cache->expire = newcred->times.endtime; kcm_update_renew_time(cache); } else if (newcred->flags.b.renewable) { cache->expire = newcred->times.endtime; kcm_update_renew_time(cache); kcm_update_expire_time(cache, newcred->times.endtime); } else if (newcred->times.endtime > time(NULL)) { cache->expire = newcred->times.endtime; kcm_update_expire_time(cache, newcred->times.endtime); } notify_changed_caches(); return 0; } krb5_error_code kcm_ccache_remove_creds_internal(krb5_context context, kcm_ccache ccache) { struct kcm_creds *k; k = ccache->creds; while (k != NULL) { struct kcm_creds *old; krb5_free_cred_contents(context, &k->cred); old = k; k = k->next; free(old); } ccache->creds = NULL; notify_changed_caches(); return 0; } krb5_error_code kcm_ccache_remove_creds(krb5_context context, kcm_ccache ccache) { krb5_error_code ret; KCM_ASSERT_VALID(ccache); HEIMDAL_MUTEX_lock(&ccache->mutex); ret = kcm_ccache_remove_creds_internal(context, ccache); HEIMDAL_MUTEX_unlock(&ccache->mutex); return ret; } krb5_error_code kcm_zero_ccache_data_internal(krb5_context context, kcm_ccache cache) { if (cache->client != NULL) { krb5_free_principal(context, cache->client); cache->client = NULL; } if (cache->server != NULL) { krb5_free_principal(context, cache->server); cache->server = NULL; } kcm_ccache_remove_creds_internal(context, cache); return 0; } krb5_error_code kcm_zero_ccache_data(krb5_context context, kcm_ccache cache) { krb5_error_code ret; KCM_ASSERT_VALID(cache); HEIMDAL_MUTEX_lock(&cache->mutex); ret = kcm_zero_ccache_data_internal(context, cache); HEIMDAL_MUTEX_unlock(&cache->mutex); return ret; } krb5_error_code kcm_retain_ccache(krb5_context context, kcm_ccache ccache) { KCM_ASSERT_VALID(ccache); HEIMDAL_MUTEX_lock(&ccache->mutex); ccache->refcnt++; HEIMDAL_MUTEX_unlock(&ccache->mutex); return 0; } static void kcm_release_ccache_locked(krb5_context context, kcm_ccache p) { if (p->refcnt == 1) { kcm_free_ccache_data_internal(context, p); HEIMDAL_MUTEX_unlock(&p->mutex); HEIMDAL_MUTEX_destroy(&p->mutex); free(p); } else { p->refcnt--; HEIMDAL_MUTEX_unlock(&p->mutex); } } krb5_error_code kcm_release_ccache(krb5_context context, kcm_ccache c) { KCM_ASSERT_VALID(c); HEIMDAL_MUTEX_lock(&c->mutex); kcm_release_ccache_locked(context, c); return 0; } krb5_error_code kcm_ccache_new(krb5_context context, const char *name, kcm_ccache *ccache) { krb5_error_code ret; ret = kcm_ccache_alloc(context, name, ccache); if (ret == 0) { /* * one reference is held by the linked list, * one by the caller */ kcm_retain_ccache(context, *ccache); } return ret; } krb5_error_code kcm_ccache_store_cred(krb5_context context, kcm_ccache ccache, krb5_creds *creds, int copy) { krb5_error_code ret; KCM_ASSERT_VALID(ccache); HEIMDAL_MUTEX_lock(&ccache->mutex); ret = kcm_ccache_store_cred_internal(context, ccache, creds, NULL, copy); HEIMDAL_MUTEX_unlock(&ccache->mutex); return ret; } struct kcm_creds * kcm_ccache_find_cred_uuid(krb5_context context, kcm_ccache ccache, kcmuuid_t uuid) { struct kcm_creds *c; for (c = ccache->creds; c != NULL; c = c->next) if (memcmp(c->uuid, uuid, sizeof(c->uuid)) == 0) return c; return NULL; } krb5_error_code kcm_ccache_store_cred_internal(krb5_context context, kcm_ccache ccache, krb5_creds *creds, kcmuuid_t uuid, int copy) { struct kcm_creds **c; krb5_error_code ret; /* * Remove dup creds and find the end to add new credential. */ c = &ccache->creds; while (*c != NULL) { if (krb5_compare_creds(context, 0, creds, &(*c)->cred)) { struct kcm_creds *dup_cred = *c; *c = dup_cred->next; krb5_free_cred_contents(context, &dup_cred->cred); free(dup_cred); } else { c = &(*c)->next; } } *c = (struct kcm_creds *)calloc(1, sizeof(**c)); if (*c == NULL) return KRB5_CC_NOMEM; if (uuid) memcpy((*c)->uuid, uuid, sizeof((*c)->uuid)); else CCRandomCopyBytes(kCCRandomDefault, (*c)->uuid, sizeof((*c)->uuid)); if (copy) { ret = krb5_copy_creds_contents(context, creds, &(*c)->cred); if (ret) { free(*c); *c = NULL; } } else { (*c)->cred = *creds; ret = 0; } /* * Only push notification when the krbtgt in the cache changes. */ if ((*c)->cred.server && krb5_principal_is_root_krbtgt(context, (*c)->cred.server)) notify_changed_caches(); return ret; } krb5_error_code kcm_ccache_remove_cred_internal(krb5_context context, kcm_ccache ccache, krb5_flags whichfields, const krb5_creds *mcreds) { krb5_error_code ret; struct kcm_creds **c; ret = KRB5_CC_NOTFOUND; for (c = &ccache->creds; *c != NULL; c = &(*c)->next) { if (krb5_compare_creds(context, whichfields, mcreds, &(*c)->cred)) { struct kcm_creds *cred = *c; *c = cred->next; krb5_free_cred_contents(context, &cred->cred); free(cred); ret = 0; if (*c == NULL) break; } } notify_changed_caches(); return ret; } krb5_error_code kcm_ccache_remove_cred(krb5_context context, kcm_ccache ccache, krb5_flags whichfields, const krb5_creds *mcreds) { krb5_error_code ret; KCM_ASSERT_VALID(ccache); HEIMDAL_MUTEX_lock(&ccache->mutex); ret = kcm_ccache_remove_cred_internal(context, ccache, whichfields, mcreds); HEIMDAL_MUTEX_unlock(&ccache->mutex); return ret; } krb5_error_code kcm_ccache_retrieve_cred_internal(krb5_context context, kcm_ccache ccache, krb5_flags whichfields, const krb5_creds *mcreds, krb5_creds **creds) { krb5_boolean match; struct kcm_creds *c; krb5_error_code ret; memset(creds, 0, sizeof(*creds)); ret = KRB5_CC_END; match = FALSE; for (c = ccache->creds; c != NULL; c = c->next) { match = krb5_compare_creds(context, whichfields, mcreds, &c->cred); if (match) break; } if (match) { ret = 0; *creds = &c->cred; } return ret; } krb5_error_code kcm_ccache_retrieve_cred(krb5_context context, kcm_ccache ccache, krb5_flags whichfields, const krb5_creds *mcreds, krb5_creds **credp) { krb5_error_code ret; KCM_ASSERT_VALID(ccache); HEIMDAL_MUTEX_lock(&ccache->mutex); ret = kcm_ccache_retrieve_cred_internal(context, ccache, whichfields, mcreds, credp); HEIMDAL_MUTEX_unlock(&ccache->mutex); return ret; } char * kcm_ccache_first_name(kcm_client *client) { kcm_ccache p; char *name = NULL; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH(p, &ccache_head, members) { if (kcm_is_same_session(client, p->uid, p->session)) break; } if (p) name = strdup(p->name); HEIMDAL_MUTEX_unlock(&ccache_mutex); return name; } void kcm_cache_remove_session(pid_t session) { kcm_ccache p, tempp; HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH_SAFE(p, &ccache_head, members, tempp) { if (p->session == session) { kcm_log(1, "remove credental %s because session %d went away", p->name, (int)session); TAILQ_REMOVE(&ccache_head, p, members); dispatch_async(dispatch_get_main_queue(), ^{ /* XXX blocking */ HEIMDAL_MUTEX_lock(&p->mutex); heim_ipc_event_cancel(p->renew_event); heim_ipc_event_free(p->renew_event); p->renew_event = NULL; heim_ipc_event_cancel(p->expire_event); heim_ipc_event_free(p->expire_event); p->expire_event = NULL; kcm_release_ccache_locked(kcm_context, p); }); } } HEIMDAL_MUTEX_unlock(&ccache_mutex); } static bool session_exists(pid_t asid) { auditinfo_addr_t aia; aia.ai_asid = asid; if (audit_get_sinfo_addr(&aia, sizeof(aia)) == 0) return true; return false; } #define CHECK(s) do { if ((s)) { goto out; } } while(0) #define DUMP_F_SERVER 1 #define DUMP_F_PASSWORD 2 #define DUMP_F_KEYTAB 4 static krb5_error_code parse_krb5_cache(krb5_context context, krb5_storage *sp) { krb5_error_code ret; kcm_ccache c = NULL; char *name = NULL; uint32_t u32; int32_t s32; uint8_t u8; time_t renew_time; CHECK(ret = krb5_ret_stringz(sp, &name)); ret = kcm_ccache_new(context, name, &c); free(name); CHECK(ret); CHECK(ret = krb5_ret_uuid(sp, c->uuid)); CHECK(ret = krb5_ret_uint32(sp, &u32)); c->renew_time = renew_time = u32; CHECK(ret = krb5_ret_uint32(sp, &u32)); c->next_refresh_time = u32; CHECK(ret = krb5_ret_uint32(sp, &u32)); c->holdcount = u32; CHECK(ret = krb5_ret_uint32(sp, &u32)); c->flags = u32; CHECK(ret = krb5_ret_int32(sp, &s32)); c->uid = s32; CHECK(ret = krb5_ret_int32(sp, &s32)); c->session = s32; CHECK(ret = krb5_ret_principal(sp, &c->client)); CHECK(ret = krb5_ret_uint32(sp, &u32)); if (u32 & DUMP_F_SERVER) CHECK(ret = krb5_ret_principal(sp, &c->server)); if (u32 & DUMP_F_PASSWORD) CHECK(ret = krb5_ret_stringz(sp, &c->password)); if (u32 & DUMP_F_KEYTAB) { char *keytab; CHECK(ret = krb5_ret_stringz(sp, &keytab)); CHECK(ret = krb5_kt_resolve(context, keytab, &c->keytab)); free(keytab); } while (1) { krb5_creds cred; kcmuuid_t uuid; CHECK(ret = krb5_ret_uint8(sp, &u8)); if (u8 == 0) break; CHECK(ret = krb5_ret_uuid(sp, uuid)); CHECK(ret = krb5_ret_creds(sp, &cred)); ret = kcm_ccache_store_cred_internal(context, c, &cred, uuid, 1); krb5_free_cred_contents(context, &cred); CHECK(ret); } /* if we have a renew time and its not too far in the past, kick off rewtime again */ if (renew_time && renew_time > time(NULL) - 60) { kcm_log(1, "re-setting renew time to: %ds (original renew time)", (int)(renew_time - time(NULL))); heim_ipc_event_set_time(c->renew_event, renew_time); } if (c->next_refresh_time) kcm_update_expire_time(c, c->next_refresh_time); out: /* in case of failure, abandon memory (and broken cache) */ if (ret && c) { TAILQ_REMOVE(&ccache_head, c, members); } return ret; } static void update_wakeup(time_t *nextwakeup, time_t t) { /* * don't bother wakeing up if its within 30s, to sort of time anyway, * just let the credential expire */ if (t < time(NULL) - 30) return; if (*nextwakeup == 0 || *nextwakeup > t) *nextwakeup = t; } static krb5_error_code unparse_krb5_cache(krb5_context context, krb5_storage *sp, kcm_ccache c, time_t *nextwakeup) { struct kcm_creds *cred; krb5_error_code ret; uint32_t sflags; if (c->renew_time) update_wakeup(nextwakeup, c->renew_time); if ((c->flags & KCM_MASK_KEY_PRESENT) && c->next_refresh_time) update_wakeup(nextwakeup, c->next_refresh_time); CHECK(ret = krb5_store_stringz(sp, c->name)); CHECK(ret = krb5_store_uuid(sp, c->uuid)); CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->renew_time)); CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->next_refresh_time)); CHECK(ret = krb5_store_uint32(sp, (uint32_t)c->holdcount)); CHECK(ret = krb5_store_uint32(sp, c->flags)); CHECK(ret = krb5_store_int32(sp, c->uid)); CHECK(ret = krb5_store_int32(sp, c->session)); CHECK(ret = krb5_store_principal(sp, c->client)); sflags = 0; if (c->server) sflags |= DUMP_F_SERVER; if (c->password) sflags |= DUMP_F_PASSWORD; if (c->keytab) sflags |= DUMP_F_KEYTAB; CHECK(ret = krb5_store_uint32(sp, sflags)); if (c->server) CHECK(ret = krb5_store_principal(sp, c->server)); if (c->password) CHECK(ret = krb5_store_stringz(sp, c->password)); if (c->keytab) { char *str; CHECK(ret = krb5_kt_get_full_name(context, c->keytab, &str)); CHECK(ret = krb5_store_stringz(sp, str)); krb5_xfree(str); } for (cred = c->creds; cred != NULL; cred = cred->next) { CHECK(ret = krb5_store_uint8(sp, 1)); CHECK(ret = krb5_store_uuid(sp, cred->uuid)); CHECK(ret = krb5_store_creds(sp, &cred->cred)); } CHECK(ret = krb5_store_uint8(sp, 0)); out: return ret; } static krb5_error_code parse_default_one(krb5_context context, krb5_storage *sp) { struct kcm_default_cache *c; krb5_error_code ret; int32_t s32; char *str; c = calloc(1, sizeof(*c)); if (c == NULL) return ENOMEM; CHECK(ret = krb5_ret_int32(sp, &s32)); c->uid = s32; CHECK(ret = krb5_ret_int32(sp, &s32)); c->session = s32; CHECK(ret = krb5_ret_stringz(sp, &str)); c->name = str; c->next = default_caches; default_caches = c; out: if (ret) kcm_log(10, "failed to parse default entry"); return ret; } static krb5_error_code unparse_default_one(krb5_storage *inner, struct kcm_default_cache *c) { krb5_error_code ret; CHECK(ret = krb5_store_int32(inner, c->uid)); CHECK(ret = krb5_store_int32(inner, c->session)); CHECK(ret = krb5_store_stringz(inner, c->name)); out: return ret; } static krb5_error_code unparse_default_all(krb5_context context, krb5_storage *sp) { struct kcm_default_cache *c; krb5_error_code r = 0; for (c = default_caches; r == 0 && c != NULL; c = c->next) { r = kcm_unparse_wrap(sp, "default-cache", c->session, ^(krb5_storage *inner) { return unparse_default_one(inner, c); }); } return r; } #define KCM_DUMP_VERSION 2 static int kcm_parse_cache_data(krb5_context context, krb5_data *data) { krb5_error_code ret; krb5_storage *sp; char *str; uint8_t u8; sp = krb5_storage_from_readonly_mem(data->data, data->length); if (sp == NULL) return ENOMEM; CHECK(ret = krb5_ret_stringz(sp, &str)); if (strcmp(str, "start-dump") != 0) { free(str); ret = EINVAL; goto out; } free(str); CHECK(ret = krb5_ret_uint8(sp, &u8)); if (u8 != KCM_DUMP_VERSION) { ret = EINVAL; goto out; } CHECK(ret = krb5_ret_uint32(sp, &ccache_nextid)); while(ret == 0) { int32_t session; krb5_data idata; krb5_storage *inner; CHECK(ret = krb5_ret_stringz(sp, &str)); kcm_log(10, "dump: reading a %s entry", str); if (strcmp(str, "end-dump") == 0) { free(str); break; } CHECK(ret = krb5_ret_int32(sp, &session)); CHECK(ret = krb5_ret_data(sp, &idata)); inner = krb5_storage_from_data(&idata); heim_assert(inner, "krb5_storage_from_data"); if (strcmp(str, "ntlm-cache") == 0) { ret = kcm_parse_ntlm_challenge_one(context, inner); } else if (!session_exists(session)) { /* do nothing */ } else if (strcmp(str, "krb5-cache") == 0) { ret = parse_krb5_cache(context, inner); } else if (strcmp(str, "digest-cache") == 0) { ret = kcm_parse_digest_one(context, inner); } else if (strcmp(str, "default-cache") == 0) { ret = parse_default_one(context, inner); } else { kcm_log(10, "dump: unknown type: %s", str); ret = 0; } if (ret) kcm_log(10, "dump: failed to unparse a %s cache with: %d", str, ret); free(str); krb5_storage_free(inner); krb5_data_free(&idata); } out: krb5_storage_free(sp); if (ret) kcm_log(10, "dump: failed to read credential dump: %d", ret); return ret; } krb5_error_code kcm_unparse_wrap(krb5_storage *sp, char *name, int32_t session, int (^wrapped)(krb5_storage *inner)) { krb5_error_code ret; krb5_storage *inner = krb5_storage_emem(); krb5_data data; CHECK(ret = wrapped(inner)); CHECK(ret = krb5_store_stringz(sp, name)); CHECK(ret = krb5_store_int32(sp, session)); CHECK(ret = krb5_storage_to_data(inner, &data)); ret = krb5_store_data(sp, data); krb5_data_free(&data); out: if (ret) kcm_log(10, "dump: failed to add a %s", name); krb5_storage_free(inner); return ret; } void kcm_unparse_cache_data(krb5_context context, krb5_data *data) { __block time_t nextwakeup = 0; krb5_error_code ret; krb5_storage *sp; kcm_ccache c; krb5_data_zero(data); sp = krb5_storage_emem(); if (sp == NULL) return; CHECK(ret = krb5_store_stringz(sp, "start-dump")); CHECK(ret = krb5_store_uint8(sp, KCM_DUMP_VERSION)); CHECK(ret = krb5_store_uint32(sp, ccache_nextid)); HEIMDAL_MUTEX_lock(&ccache_mutex); TAILQ_FOREACH_REVERSE(c, &ccache_head, ccache_head, members) { ret = kcm_unparse_wrap(sp, "krb5-cache", c->session, ^(krb5_storage *inner) { return unparse_krb5_cache(context, inner, c, &nextwakeup); }); } HEIMDAL_MUTEX_unlock(&ccache_mutex); CHECK(ret); /* add default cache */ CHECK(ret = unparse_default_all(context, sp)); /* add NTLM/SCRAM */ CHECK(ret = kcm_unparse_digest_all(context, sp)); /* ntlm challenges */ CHECK(ret = kcm_unparse_challenge_all(context, sp)); CHECK(ret = krb5_store_stringz(sp, "end-dump")); if (nextwakeup) { int64_t next = nextwakeup - time(NULL); if (next > 0) { vproc_swap_integer(NULL, VPROC_GSK_START_INTERVAL, &next, NULL); } kcm_log(1, "next wakup in: %d", (int)next); } out: if (ret == 0) { ret = krb5_storage_to_data(sp, data); if (ret) kcm_log(1, "dump: failed to create credential data: %d", ret); } krb5_storage_free(sp); } static int have_uuid_master = 0; static krb5_uuid uuid_master; static const char *dumpfile = "/var/db/kcm-dump.bin"; static const char *keyfile = "/var/db/kcm-dump.uuid"; static krb5_error_code kcm_load_key(krb5_context context) { krb5_error_code ret; krb5_data enc; size_t len; void *p = NULL; if (have_uuid_master) return 0; ret = rk_undumpdata(keyfile, &p, &len); if (ret != 0) goto nokey; if (len != sizeof(uuid_master)) { free(p); goto nokey; } memcpy(uuid_master, p, sizeof(uuid_master)); free(p); /* check if key is stale */ ret = kcm_store_io(context, uuid_master, "", 0, &enc, true); if (ret) goto nokey; krb5_data_free(&enc); have_uuid_master = 1; return 0; nokey: ret = kcm_create_key(uuid_master); if (ret) return ret; rk_dumpdata(keyfile, uuid_master, sizeof(uuid_master)); have_uuid_master = 1; return ret; } int kcm_data_changed = 0; void kcm_write_dump(krb5_context context) { uuid_string_t uuidstr; krb5_data data, enc; krb5_error_code ret; ret = kcm_load_key(context); if (ret) { unlink(keyfile); unlink(dumpfile); return; } uuid_unparse(uuid_master, uuidstr); kcm_log(10, "dump: [masterkey] %s", uuidstr); kcm_unparse_cache_data(context, &data); if (data.length == 0) return; if (!kcm_data_changed) return; ret = kcm_store_io(context, uuid_master, data.data, data.length, &enc, true); krb5_data_free(&data); if (ret) { kcm_log(1, "dump: failed to encrypt credential data %d", ret); return; } rk_dumpdata(dumpfile, enc.data, enc.length); krb5_data_free(&enc); } void kcm_read_dump(krb5_context context) { uuid_string_t uuidstr; krb5_error_code ret; krb5_data data; size_t len; void *p; ret = kcm_load_key(context); if (ret) return; uuid_unparse(uuid_master, uuidstr); kcm_log(10, "load: [masterkey] %s", uuidstr); ret = rk_undumpdata(dumpfile, &p, &len); if (ret != 0 || len == 0) return; ret = kcm_store_io(kcm_context, uuid_master, p, len, &data, false); if (ret == 0) { ret = kcm_parse_cache_data(kcm_context, &data); if (ret) unlink(dumpfile); krb5_data_free(&data); have_uuid_master = 1; } else { unlink(dumpfile); } free(p); }