--- /dev/null
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krb5/ccache/cc_api_macos.c - Native MacOS X ccache code */
+/*
+ * Copyright (C) 2022 United States Government as represented by the
+ * Secretary of the Navy.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This ccache module provides compatibility with the default native ccache
+ * type for macOS, by linking against the native Kerberos framework and calling
+ * the CCAPI stubs. Due to workarounds for specific behaviors of the CCAPI
+ * stubs, this implementation is separate from the API ccache implementation
+ * used on Windows.
+ */
+
+#include "k5-int.h"
+#include "cc-int.h"
+#include "ccapi_util.h"
+#include <CredentialsCache.h>
+
+#ifdef USE_CCAPI_MACOS
+
+#include <sys/utsname.h>
+#include <xpc/xpc.h>
+
+const krb5_cc_ops krb5_api_macos_ops;
+
+struct api_macos_cache_data {
+ char *residual;
+ cc_context_t cc_context;
+ cc_ccache_t cache;
+};
+
+struct api_macos_ptcursor {
+ krb5_boolean first;
+ char *primary;
+ cc_context_t cc_context;
+ cc_ccache_iterator_t iter;
+};
+
+/* Map a CCAPI error code to a com_err code. */
+static krb5_error_code
+ccerr2mit(uint32_t err)
+{
+ switch (err) {
+ case ccNoError:
+ return 0;
+ case ccIteratorEnd:
+ return KRB5_CC_END;
+ case ccErrNoMem:
+ return ENOMEM;
+ case ccErrCCacheNotFound:
+ return KRB5_FCC_NOFILE;
+ default:
+ return KRB5_FCC_INTERNAL;
+ }
+}
+
+/* Construct a ccache handle for residual. Use cc_context if it is not null,
+ * or initialize a new one if it is. */
+static krb5_error_code
+make_cache(const char *residual, cc_context_t cc_context,
+ krb5_ccache *ccache_out)
+{
+ krb5_ccache cache = NULL;
+ char *residual_copy = NULL;
+ struct api_macos_cache_data *data = NULL;
+ uint32_t err;
+
+ *ccache_out = NULL;
+
+ if (cc_context == NULL) {
+ err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL);
+ if (err != ccNoError)
+ return KRB5_FCC_INTERNAL;
+ }
+
+ cache = malloc(sizeof(*cache));
+ if (cache == NULL)
+ goto oom;
+
+ data = calloc(1, sizeof(*data));
+ if (data == NULL)
+ goto oom;
+
+ residual_copy = strdup(residual);
+ if (residual_copy == NULL)
+ goto oom;
+
+ data->residual = residual_copy;
+ data->cc_context = cc_context;
+ cache->ops = &krb5_api_macos_ops;
+ cache->data = data;
+ cache->magic = KV5M_CCACHE;
+ *ccache_out = cache;
+ return 0;
+
+oom:
+ free(cache);
+ free(data);
+ free(residual_copy);
+ if (cc_context)
+ cc_context_release(cc_context);
+ return ENOMEM;
+}
+
+static uint32_t
+open_cache(struct api_macos_cache_data *data)
+{
+ if (data->cache != NULL)
+ return ccNoError;
+ return cc_context_open_ccache(data->cc_context, data->residual,
+ &data->cache);
+}
+
+static const char *
+api_macos_get_name(krb5_context context, krb5_ccache ccache)
+{
+ struct api_macos_cache_data *data = ccache->data;
+
+ return data->residual;
+}
+
+/*
+ * We would like to use cc_context_get_default_ccache_name() for this, but that
+ * doesn't work on macOS if the default cache name is set by the environment or
+ * configuration. So we have to do what the underlying macOS Heimdal API cache
+ * type does to fetch the primary name.
+ *
+ * For macOS 11 (Darwin 20) and later, implement just enough of the XCACHE
+ * protocol to fetch the primary UUID. For earlier versions, query the KCM
+ * daemon.
+ */
+static krb5_error_code
+get_primary_name(krb5_context context, char **name_out)
+{
+ krb5_error_code ret;
+ xpc_connection_t conn = NULL;
+ xpc_object_t request = NULL, reply = NULL;
+ const uint8_t *uuid;
+ uint64_t flags = XPC_CONNECTION_MACH_SERVICE_PRIVILEGED;
+ char uuidstr[37], *end;
+ struct utsname un;
+ long release;
+
+ *name_out = NULL;
+
+ if (uname(&un) == 0) {
+ release = strtol(un.release, &end, 10);
+ if (end != un.release && release < 20) {
+ /* Query the KCM daemon for macOS 10 and earlier. */
+ ret = k5_kcm_primary_name(context, name_out);
+ goto cleanup;
+ }
+ }
+
+ conn = xpc_connection_create_mach_service("com.apple.GSSCred", NULL,
+ flags);
+ if (conn == NULL) {
+ ret = ENOMEM;
+ goto cleanup;
+ }
+ xpc_connection_set_event_handler(conn, ^(xpc_object_t o){ ; });
+ xpc_connection_resume(conn);
+
+ request = xpc_dictionary_create(NULL, NULL, 0);
+ if (request == NULL) {
+ ret = ENOMEM;
+ goto cleanup;
+ }
+ xpc_dictionary_set_string(request, "command", "default");
+ xpc_dictionary_set_string(request, "mech", "kHEIMTypeKerberos");
+
+ reply = xpc_connection_send_message_with_reply_sync(conn, request);
+ if (reply == NULL || xpc_get_type(reply) == XPC_TYPE_ERROR) {
+ ret = KRB5_CC_IO;
+ goto cleanup;
+ }
+
+ uuid = xpc_dictionary_get_uuid(reply, "default");
+ if (uuid == NULL) {
+ ret = KRB5_CC_IO;
+ goto cleanup;
+ }
+ uuid_unparse(uuid, uuidstr);
+
+ *name_out = strdup(uuidstr);
+ ret = (*name_out == NULL) ? ENOMEM : 0;
+
+cleanup:
+ if (request != NULL)
+ xpc_release(request);
+ if (reply != NULL)
+ xpc_release(reply);
+ if (conn != NULL)
+ xpc_connection_cancel(conn);
+ return ret;
+}
+
+static krb5_error_code
+api_macos_resolve(krb5_context context, krb5_ccache *cache_out,
+ const char *residual)
+{
+ krb5_error_code ret;
+ char *primary = NULL;
+
+ if (*residual == '\0') {
+ ret = get_primary_name(context, &primary);
+ if (ret)
+ return ret;
+ residual = primary;
+ }
+ ret = make_cache(residual, NULL, cache_out);
+ free(primary);
+ return ret;
+}
+
+static krb5_error_code
+api_macos_gen_new(krb5_context context, krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ uint32_t err;
+ cc_context_t cc_context = NULL;
+ cc_ccache_t cc_ccache = NULL;
+ cc_string_t cachename = NULL;
+ struct api_macos_cache_data *data;
+
+ *cache_out = NULL;
+
+ err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL);
+ if (err)
+ goto cleanup;
+
+ err = cc_context_create_new_ccache(cc_context, cc_credentials_v5, "",
+ &cc_ccache);
+ if (err)
+ goto cleanup;
+
+ err = cc_ccache_get_name(cc_ccache, &cachename);
+ if (err)
+ goto cleanup;
+
+ ret = make_cache(cachename->data, cc_context, cache_out);
+ cc_context = NULL;
+ if (!ret) {
+ data = (*cache_out)->data;
+ data->cache = cc_ccache;
+ cc_ccache = NULL;
+ }
+
+cleanup:
+ if (cc_context != NULL)
+ cc_context_release(cc_context);
+ if (cc_ccache != NULL)
+ cc_ccache_release(cc_ccache);
+ return err ? KRB5_FCC_INTERNAL : 0;
+}
+
+static krb5_error_code
+api_macos_initialize(krb5_context context, krb5_ccache cache,
+ krb5_principal princ)
+{
+ krb5_error_code ret;
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+ char *princstr = NULL, *prefix_name = NULL;
+
+ /* Apple's cc_context_create_ccache() requires a name with type prefix. */
+ if (asprintf(&prefix_name, "API:%s", data->residual) < 0)
+ return ENOMEM;
+
+ ret = krb5_unparse_name(context, princ, &princstr);
+ if (ret) {
+ free(prefix_name);
+ return ret;
+ }
+
+ if (data->cache != NULL) {
+ cc_ccache_release(data->cache);
+ data->cache = NULL;
+ }
+
+ err = cc_context_create_ccache(data->cc_context, prefix_name,
+ cc_credentials_v5, princstr,
+ &data->cache);
+ krb5_free_unparsed_name(context, princstr);
+ free(prefix_name);
+ return ccerr2mit(err);
+}
+
+static krb5_error_code
+api_macos_close(krb5_context context, krb5_ccache cache)
+{
+ struct api_macos_cache_data *data = cache->data;
+
+ if (data->cache != NULL)
+ cc_ccache_release(data->cache);
+ cc_context_release(data->cc_context);
+ free(data->residual);
+ free(data);
+ free(cache);
+ return 0;
+}
+
+static krb5_error_code
+api_macos_destroy(krb5_context context, krb5_ccache cache)
+{
+ struct api_macos_cache_data *data = cache->data;
+
+ open_cache(data);
+ if (data->cache != NULL) {
+ cc_ccache_destroy(data->cache);
+ data->cache = NULL;
+ }
+ return api_macos_close(context, cache);
+}
+
+static krb5_error_code
+api_macos_store(krb5_context context, krb5_ccache cache, krb5_creds *creds)
+{
+ struct api_macos_cache_data *data = cache->data;
+ cc_credentials_union *c_un = NULL;
+ krb5_error_code ret;
+ uint32_t err;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ ret = k5_krb5_to_ccapi_creds(context, creds, &c_un);
+ if (ret)
+ return ret;
+ err = cc_ccache_store_credentials(data->cache, c_un);
+ k5_release_ccapi_cred(c_un);
+ return ccerr2mit(err);
+}
+
+static krb5_error_code
+api_macos_retrieve(krb5_context context, krb5_ccache cache,
+ krb5_flags whichfields, krb5_creds *mcreds,
+ krb5_creds *creds)
+{
+ return k5_cc_retrieve_cred_default(context, cache, whichfields,
+ mcreds, creds);
+}
+
+static krb5_error_code
+api_macos_get_princ(krb5_context context, krb5_ccache cache,
+ krb5_principal *princ)
+{
+ struct api_macos_cache_data *data = cache->data;
+ krb5_error_code ret;
+ uint32_t err;
+ cc_string_t outprinc;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_get_principal(data->cache, cc_credentials_v5, &outprinc);
+ if (err)
+ return ccerr2mit(err);
+ ret = krb5_parse_name(context, outprinc->data, princ);
+ cc_string_release(outprinc);
+ return ret;
+}
+
+static krb5_error_code
+api_macos_start_seq_get(krb5_context context, krb5_ccache cache,
+ krb5_cc_cursor *cursor)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+ cc_credentials_iterator_t iter;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_new_credentials_iterator(data->cache, &iter);
+ if (err)
+ return ccerr2mit(err);
+
+ *cursor = (krb5_cc_cursor)iter;
+ return 0;
+}
+
+static krb5_error_code
+api_macos_next_cred(krb5_context context, krb5_ccache cache,
+ krb5_cc_cursor *cursor, krb5_creds *creds)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+ krb5_error_code ret;
+ cc_credentials_iterator_t iter = (cc_credentials_iterator_t) *cursor;
+ cc_credentials_t acreds;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_credentials_iterator_next(iter, &acreds);
+ if (!err) {
+ ret = k5_ccapi_to_krb5_creds(context, acreds->data, creds);
+ cc_credentials_release(acreds);
+ } else {
+ ret = ccerr2mit(err);
+ }
+ return ret;
+}
+
+static krb5_error_code
+api_macos_end_seq_get(krb5_context context, krb5_ccache cache,
+ krb5_cc_cursor *cursor)
+{
+ cc_credentials_iterator_t iter = *cursor;
+
+ cc_credentials_iterator_release(iter);
+ *cursor = NULL;
+ return 0;
+}
+
+static krb5_error_code
+api_macos_remove_cred(krb5_context context, krb5_ccache cache,
+ krb5_flags flags, krb5_creds *creds)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+ krb5_error_code ret = 0;
+ cc_credentials_iterator_t iter = NULL;
+ cc_credentials_t acreds;
+ krb5_creds mcreds;
+ krb5_boolean match;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_new_credentials_iterator(data->cache, &iter);
+ if (err)
+ return ccerr2mit(err);
+
+ for (;;) {
+ err = cc_credentials_iterator_next(iter, &acreds);
+ if (err)
+ break;
+
+ ret = k5_ccapi_to_krb5_creds(context, acreds->data, &mcreds);
+ if (ret) {
+ cc_credentials_release(acreds);
+ break;
+ }
+
+ match = krb5int_cc_creds_match_request(context, flags, creds, &mcreds);
+ krb5_free_cred_contents(context, &mcreds);
+ if (match)
+ err = cc_ccache_remove_credentials(data->cache, acreds);
+ cc_credentials_release(acreds);
+ if (err)
+ break;
+ }
+
+ cc_credentials_iterator_release(iter);
+
+ if (ret)
+ return ret;
+ if (err != ccIteratorEnd)
+ return ccerr2mit(err);
+ return 0;
+}
+
+static krb5_error_code
+api_macos_set_flags(krb5_context context, krb5_ccache cache, krb5_flags flags)
+{
+ return 0;
+}
+
+static krb5_error_code
+api_macos_get_flags(krb5_context context, krb5_ccache cache, krb5_flags *flags)
+{
+ *flags = 0;
+ return 0;
+}
+
+static krb5_error_code
+api_macos_ptcursor_new(krb5_context context, krb5_cc_ptcursor *ptcursor_out)
+{
+ krb5_cc_ptcursor ptcursor = NULL;
+ struct api_macos_ptcursor *apt = NULL;
+
+ apt = malloc(sizeof(*apt));
+ if (apt == NULL)
+ return ENOMEM;
+ apt->first = TRUE;
+ apt->primary = NULL;
+ apt->cc_context = NULL;
+ apt->iter = NULL;
+
+ ptcursor = malloc(sizeof(*ptcursor));
+ if (ptcursor == NULL) {
+ free(apt);
+ return ENOMEM;
+ }
+
+ ptcursor->ops = &krb5_api_macos_ops;
+ ptcursor->data = apt;
+ *ptcursor_out = ptcursor;
+ return 0;
+}
+
+/* Create a cache object and open it to ensure that it exists in the
+ * collection. If it does not, return success but set *cache_out to NULL. */
+static krb5_error_code
+make_open_cache(const char *residual, krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ krb5_ccache cache;
+ uint32_t err;
+
+ *cache_out = NULL;
+
+ ret = make_cache(residual, NULL, &cache);
+ if (ret)
+ return ret;
+
+ err = open_cache(cache->data);
+ if (err) {
+ api_macos_close(NULL, cache);
+ return (err == ccErrCCacheNotFound) ? 0 : ccerr2mit(err);
+ }
+
+ *cache_out = cache;
+ return 0;
+}
+
+static krb5_error_code
+api_macos_ptcursor_next(krb5_context context, krb5_cc_ptcursor ptcursor,
+ krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ uint32_t err;
+ struct api_macos_ptcursor *apt = ptcursor->data;
+ const char *defname, *defresidual;
+ cc_ccache_t cache;
+ cc_string_t residual;
+ struct api_macos_cache_data *data;
+
+ *cache_out = NULL;
+
+ defname = krb5_cc_default_name(context);
+ if (defname == NULL || strncmp(defname, "API:", 4) != 0)
+ return 0;
+ defresidual = defname + 4;
+
+ /* If the default cache name is a subsidiary cache, yield that cache if it
+ * exists and stop. */
+ if (*defresidual != '\0') {
+ if (!apt->first)
+ return 0;
+ apt->first = FALSE;
+ return make_open_cache(defresidual, cache_out);
+ }
+
+ if (apt->first) {
+ apt->first = FALSE;
+
+ /* Prepare to iterate over the collection. */
+ err = cc_initialize(&apt->cc_context, ccapi_version_max, NULL, NULL);
+ if (err)
+ return KRB5_FCC_INTERNAL;
+ err = cc_context_new_ccache_iterator(apt->cc_context, &apt->iter);
+ if (err)
+ return KRB5_FCC_INTERNAL;
+
+ /* Yield the primary cache first if it exists. */
+ ret = get_primary_name(context, &apt->primary);
+ if (ret)
+ return ret;
+ ret = make_open_cache(apt->primary, cache_out);
+ if (ret || *cache_out != NULL)
+ return ret;
+ }
+
+ for (;;) {
+ err = cc_ccache_iterator_next(apt->iter, &cache);
+ if (err)
+ return (err == ccIteratorEnd) ? 0 : ccerr2mit(err);
+
+ err = cc_ccache_get_name(cache, &residual);
+ if (err) {
+ cc_ccache_release(cache);
+ return ccerr2mit(err);
+ }
+
+ /* Skip the primary cache since we yielded it first. */
+ if (strcmp(residual->data, apt->primary) != 0)
+ break;
+ }
+
+ ret = make_cache(residual->data, NULL, cache_out);
+ cc_string_release(residual);
+ if (ret) {
+ cc_ccache_release(cache);
+ return ret;
+ }
+ data = (*cache_out)->data;
+ data->cache = cache;
+ return 0;
+}
+
+static krb5_error_code
+api_macos_ptcursor_free(krb5_context context, krb5_cc_ptcursor *ptcursor)
+{
+ struct api_macos_ptcursor *apt = (*ptcursor)->data;
+
+ if (apt != NULL) {
+ if (apt->iter != NULL)
+ cc_ccache_iterator_release(apt->iter);
+ if (apt->cc_context != NULL)
+ cc_context_release(apt->cc_context);
+ free(apt->primary);
+ free(apt);
+ }
+
+ free(*ptcursor);
+ *ptcursor = NULL;
+
+ return 0;
+}
+
+static krb5_error_code
+api_macos_lock(krb5_context context, krb5_ccache cache)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_lock(data->cache, cc_lock_write, cc_lock_block);
+ return ccerr2mit(err);
+}
+
+static krb5_error_code
+api_macos_unlock(krb5_context context, krb5_ccache cache)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_unlock(data->cache);
+ return ccerr2mit(err);
+}
+
+static krb5_error_code
+api_macos_switch_to(krb5_context context, krb5_ccache cache)
+{
+ struct api_macos_cache_data *data = cache->data;
+ uint32_t err;
+
+ err = open_cache(data);
+ if (err)
+ return ccerr2mit(err);
+
+ err = cc_ccache_set_default(data->cache);
+ return ccerr2mit(err);
+}
+
+const krb5_cc_ops krb5_api_macos_ops = {
+ 0,
+ "API",
+ api_macos_get_name,
+ api_macos_resolve,
+ api_macos_gen_new,
+ api_macos_initialize,
+ api_macos_destroy,
+ api_macos_close,
+ api_macos_store,
+ api_macos_retrieve,
+ api_macos_get_princ,
+ api_macos_start_seq_get,
+ api_macos_next_cred,
+ api_macos_end_seq_get,
+ api_macos_remove_cred,
+ api_macos_set_flags,
+ api_macos_get_flags,
+ api_macos_ptcursor_new,
+ api_macos_ptcursor_next,
+ api_macos_ptcursor_free,
+ NULL, /* move */
+ NULL, /* wasdefault */
+ api_macos_lock,
+ api_macos_unlock,
+ api_macos_switch_to,
+};
+
+#endif /* TARGET_OS_MAC */