--- /dev/null
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* lib/krb5/ccache/cc_kcm.c - KCM cache type (client side) */
+/*
+ * Copyright (C) 2014 by the Massachusetts Institute of Technology.
+ * 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 cache type contacts a daemon for each cache operation, using Heimdal's
+ * KCM protocol. On OS X, the preferred transport is Mach RPC; on other
+ * Unix-like platforms or if the daemon is not available via RPC, Unix domain
+ * sockets are used instead.
+ */
+
+#include "k5-int.h"
+#include "k5-input.h"
+#include "cc-int.h"
+#include "kcm.h"
+#include <sys/socket.h>
+#include <sys/un.h>
+#ifdef __APPLE__
+#include <mach/mach.h>
+#include <servers/bootstrap.h>
+#include "kcmrpc.h"
+#endif
+
+#ifndef _WIN32
+
+#define MAX_REPLY_SIZE (10 * 1024 * 1024)
+
+const krb5_cc_ops krb5_kcm_ops;
+
+struct uuid_list {
+ unsigned char *uuidbytes; /* all of the uuids concatenated together */
+ size_t count;
+ size_t pos;
+};
+
+struct kcmio {
+ int fd;
+#ifdef __APPLE__
+ mach_port_t mport;
+#endif
+};
+
+/* This structure bundles together a KCM request and reply, to minimize how
+ * much we have to declare and clean up in each method. */
+struct kcmreq {
+ struct k5buf reqbuf;
+ struct k5input reply;
+ void *reply_mem;
+};
+#define EMPTY_KCMREQ { EMPTY_K5BUF }
+
+struct kcm_cache_data {
+ char *residual; /* immutable; may be accessed without lock */
+ k5_cc_mutex lock; /* protects io and changetime */
+ struct kcmio *io;
+ krb5_timestamp changetime;
+};
+
+struct kcm_ptcursor {
+ struct uuid_list *uuids;
+ struct kcmio *io;
+};
+
+/* Map EINVAL or KRB5_CC_FORMAT to KRB5_KCM_MALFORMED_REPLY; pass through all
+ * other codes. */
+static inline krb5_error_code
+map_invalid(krb5_error_code code)
+{
+ return (code == EINVAL || code == KRB5_CC_FORMAT) ?
+ KRB5_KCM_MALFORMED_REPLY : code;
+}
+
+/* Begin a request for the given opcode. If cache is non-null, supply the
+ * cache name as a request parameter. */
+static void
+kcmreq_init(struct kcmreq *req, kcm_opcode opcode, krb5_ccache cache)
+{
+ unsigned char bytes[4];
+ const char *name;
+
+ memset(req, 0, sizeof(*req));
+
+ bytes[0] = KCM_PROTOCOL_VERSION_MAJOR;
+ bytes[1] = KCM_PROTOCOL_VERSION_MINOR;
+ store_16_be(opcode, bytes + 2);
+
+ k5_buf_init_dynamic(&req->reqbuf);
+ k5_buf_add_len(&req->reqbuf, bytes, 4);
+ if (cache != NULL) {
+ name = ((struct kcm_cache_data *)cache->data)->residual;
+ k5_buf_add_len(&req->reqbuf, name, strlen(name) + 1);
+ }
+}
+
+/* Add a 32-bit value to the request in big-endian byte order. */
+static void
+kcmreq_put32(struct kcmreq *req, uint32_t val)
+{
+ unsigned char bytes[4];
+
+ store_32_be(val, bytes);
+ k5_buf_add_len(&req->reqbuf, bytes, 4);
+}
+
+#ifdef __APPLE__
+
+/* The maximum length of an in-band request or reply as defined by the RPC
+ * protocol. */
+#define MAX_INBAND_SIZE 2048
+
+/* Connect or reconnect to the KCM daemon via Mach RPC, if possible. */
+static krb5_error_code
+kcmio_mach_connect(krb5_context context, struct kcmio *io)
+{
+ krb5_error_code ret;
+ kern_return_t st;
+ mach_port_t mport;
+ char *service;
+
+ ret = profile_get_string(context->profile, KRB5_CONF_LIBDEFAULTS,
+ KRB5_CONF_KCM_MACH_SERVICE, NULL,
+ DEFAULT_KCM_MACH_SERVICE, &service);
+ if (ret)
+ return ret;
+ if (strcmp(service, "-") == 0) {
+ profile_release_string(service);
+ return KRB5_KCM_NO_SERVER;
+ }
+
+ st = bootstrap_look_up(bootstrap_port, service, &mport);
+ profile_release_string(service);
+ if (st)
+ return KRB5_KCM_NO_SERVER;
+ if (io->mport != MACH_PORT_NULL)
+ mach_port_deallocate(mach_task_self(), io->mport);
+ io->mport = mport;
+ return 0;
+}
+
+/* Invoke the Mach RPC to get a reply from the KCM daemon. */
+static krb5_error_code
+kcmio_mach_call(krb5_context context, struct kcmio *io, void *data,
+ size_t len, void **reply_out, size_t *len_out)
+{
+ krb5_error_code ret;
+ size_t inband_req_len = 0, outband_req_len = 0, reply_len;
+ char *inband_req = NULL, *outband_req = NULL, *outband_reply, *copy;
+ char inband_reply[MAX_INBAND_SIZE];
+ mach_msg_type_number_t inband_reply_len, outband_reply_len;
+ const void *reply;
+ kern_return_t st;
+ int code;
+
+ *reply_out = NULL;
+ *len_out = 0;
+
+ /* Use the in-band or out-of-band request buffer depending on len. */
+ if (len <= MAX_INBAND_SIZE) {
+ inband_req = data;
+ inband_req_len = len;
+ } else {
+ outband_req = data;
+ outband_req_len = len;
+ }
+
+ st = k5_kcmrpc_call(io->mport, inband_req, inband_req_len, outband_req,
+ outband_req_len, &code, inband_reply,
+ &inband_reply_len, &outband_reply, &outband_reply_len);
+ if (st == MACH_SEND_INVALID_DEST) {
+ /* Get a new port and try again. */
+ st = kcmio_mach_connect(context, io);
+ if (st)
+ return KRB5_KCM_RPC_ERROR;
+ st = k5_kcmrpc_call(io->mport, inband_req, inband_req_len, outband_req,
+ outband_req_len, &code, inband_reply,
+ &inband_reply_len, &outband_reply,
+ &outband_reply_len);
+ }
+ if (st)
+ return KRB5_KCM_RPC_ERROR;
+
+ if (code) {
+ ret = code;
+ goto cleanup;
+ }
+
+ /* The reply could be in the in-band or out-of-band reply buffer. */
+ reply = outband_reply_len ? outband_reply : inband_reply;
+ reply_len = outband_reply_len ? outband_reply_len : inband_reply_len;
+ copy = k5memdup(reply, reply_len, &ret);
+ if (copy == NULL)
+ goto cleanup;
+
+ *reply_out = copy;
+ *len_out = reply_len;
+
+cleanup:
+ if (outband_reply_len) {
+ vm_deallocate(mach_task_self(), (vm_address_t)outband_reply,
+ outband_reply_len);
+ }
+ return ret;
+}
+
+/* Release any Mach RPC state within io. */
+static void
+kcmio_mach_close(struct kcmio *io)
+{
+ if (io->mport != MACH_PORT_NULL)
+ mach_port_deallocate(mach_task_self(), io->mport);
+}
+
+#else /* __APPLE__ */
+
+#define kcmio_mach_connect(context, io) EINVAL
+#define kcmio_mach_call(context, io, data, len, reply_out, len_out) EINVAL
+#define kcmio_mach_close(io)
+
+#endif
+
+/* Connect to the KCM daemon via a Unix domain socket. */
+static krb5_error_code
+kcmio_unix_socket_connect(krb5_context context, struct kcmio *io)
+{
+ krb5_error_code ret;
+ int fd = -1;
+ struct sockaddr_un addr;
+ char *path = NULL;
+
+ ret = profile_get_string(context->profile, KRB5_CONF_LIBDEFAULTS,
+ KRB5_CONF_KCM_SOCKET, NULL,
+ DEFAULT_KCM_SOCKET_PATH, &path);
+ if (ret)
+ goto cleanup;
+ if (strcmp(path, "-") == 0) {
+ ret = KRB5_KCM_NO_SERVER;
+ goto cleanup;
+ }
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1) {
+ ret = errno;
+ goto cleanup;
+ }
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strlcpy(addr.sun_path, path, sizeof(addr.sun_path));
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ ret = (errno == ENOENT) ? KRB5_KCM_NO_SERVER : errno;
+ goto cleanup;
+ }
+
+ io->fd = fd;
+ fd = -1;
+
+cleanup:
+ if (fd != -1)
+ close(fd);
+ profile_release_string(path);
+ return ret;
+}
+
+/* Write a KCM request: 4-byte big-endian length, then the marshalled
+ * request. */
+static krb5_error_code
+kcmio_unix_socket_write(krb5_context context, struct kcmio *io, void *request,
+ size_t len)
+{
+ char lenbytes[4];
+
+ store_32_be(len, lenbytes);
+ if (krb5_net_write(context, io->fd, lenbytes, 4) < 0)
+ return errno;
+ if (krb5_net_write(context, io->fd, request, len) < 0)
+ return errno;
+ return 0;
+}
+
+/* Read a KCM reply: 4-byte big-endian length, 4-byte big-endian status code,
+ * then the marshalled reply. */
+static krb5_error_code
+kcmio_unix_socket_read(krb5_context context, struct kcmio *io,
+ void **reply_out, size_t *len_out)
+{
+ krb5_error_code code;
+ char lenbytes[4], codebytes[4], *reply;
+ size_t len;
+ int st;
+
+ *reply_out = NULL;
+ *len_out = 0;
+
+ st = krb5_net_read(context, io->fd, lenbytes, 4);
+ if (st != 4)
+ return (st == -1) ? errno : KRB5_CC_IO;
+ len = load_32_be(lenbytes);
+ if (len > MAX_REPLY_SIZE)
+ return KRB5_KCM_REPLY_TOO_BIG;
+
+ st = krb5_net_read(context, io->fd, codebytes, 4);
+ if (st != 4)
+ return (st == -1) ? errno : KRB5_CC_IO;
+ code = load_32_be(codebytes);
+ if (code != 0)
+ return code;
+
+ reply = malloc(len);
+ if (reply == NULL)
+ return ENOMEM;
+ st = krb5_net_read(context, io->fd, reply, len);
+ if (st == -1 || (size_t)st != len) {
+ free(reply);
+ return (st < 0) ? errno : KRB5_CC_IO;
+ }
+
+ *reply_out = reply;
+ *len_out = len;
+ return 0;
+}
+
+static krb5_error_code
+kcmio_connect(krb5_context context, struct kcmio **io_out)
+{
+ krb5_error_code ret;
+ struct kcmio *io;
+
+ *io_out = NULL;
+ io = calloc(1, sizeof(*io));
+ if (io == NULL)
+ return ENOMEM;
+ io->fd = -1;
+
+ /* Try Mach RPC (OS X only), then fall back to Unix domain sockets */
+ ret = kcmio_mach_connect(context, io);
+ if (ret)
+ ret = kcmio_unix_socket_connect(context, io);
+ if (ret) {
+ free(io);
+ return ret;
+ }
+
+ *io_out = io;
+ return 0;
+}
+
+/* Check req->reqbuf for an error condition and return it. Otherwise, send the
+ * request to the KCM daemon and get a response. */
+static krb5_error_code
+kcmio_call(krb5_context context, struct kcmio *io, struct kcmreq *req)
+{
+ krb5_error_code ret;
+ size_t reply_len;
+
+ if (k5_buf_status(&req->reqbuf) != 0)
+ return ENOMEM;
+
+ if (io->fd != -1) {
+ ret = kcmio_unix_socket_write(context, io, req->reqbuf.data,
+ req->reqbuf.len);
+ if (ret)
+ return ret;
+ ret = kcmio_unix_socket_read(context, io, &req->reply_mem, &reply_len);
+ if (ret)
+ return ret;
+ } else {
+ /* We must be using Mach RPC. */
+ ret = kcmio_mach_call(context, io, req->reqbuf.data, req->reqbuf.len,
+ &req->reply_mem, &reply_len);
+ if (ret)
+ return ret;
+ }
+
+ /* Read the status code from the marshalled reply. */
+ k5_input_init(&req->reply, req->reply_mem, reply_len);
+ ret = k5_input_get_uint32_be(&req->reply);
+ return req->reply.status ? KRB5_KCM_MALFORMED_REPLY : ret;
+}
+
+static void
+kcmio_close(struct kcmio *io)
+{
+ if (io != NULL) {
+ kcmio_mach_close(io);
+ if (io->fd != -1)
+ close(io->fd);
+ free(io);
+ }
+}
+
+/* Fetch a zero-terminated name string from req->reply. The returned pointer
+ * is an alias and must not be freed by the caller. */
+static krb5_error_code
+kcmreq_get_name(struct kcmreq *req, const char **name_out)
+{
+ const unsigned char *end;
+ struct k5input *in = &req->reply;
+
+ *name_out = NULL;
+ end = memchr(in->ptr, '\0', in->len);
+ if (end == NULL)
+ return KRB5_KCM_MALFORMED_REPLY;
+ *name_out = (const char *)in->ptr;
+ (void)k5_input_get_bytes(in, end + 1 - in->ptr);
+ return 0;
+}
+
+/* Fetch a UUID list from req->reply. UUID lists are not delimited, so we
+ * consume the rest of the input. */
+static krb5_error_code
+kcmreq_get_uuid_list(struct kcmreq *req, struct uuid_list **uuids_out)
+{
+ struct uuid_list *uuids;
+
+ *uuids_out = NULL;
+
+ if (req->reply.len % KCM_UUID_LEN != 0)
+ return KRB5_KCM_MALFORMED_REPLY;
+
+ uuids = malloc(sizeof(*uuids));
+ if (uuids == NULL)
+ return ENOMEM;
+ uuids->count = req->reply.len / KCM_UUID_LEN;
+ uuids->pos = 0;
+
+ if (req->reply.len > 0) {
+ uuids->uuidbytes = malloc(req->reply.len);
+ if (uuids->uuidbytes == NULL) {
+ free(uuids);
+ return ENOMEM;
+ }
+ memcpy(uuids->uuidbytes, req->reply.ptr, req->reply.len);
+ (void)k5_input_get_bytes(&req->reply, req->reply.len);
+ } else {
+ uuids->uuidbytes = NULL;
+ }
+
+ *uuids_out = uuids;
+ return 0;
+}
+
+static void
+free_uuid_list(struct uuid_list *uuids)
+{
+ if (uuids != NULL)
+ free(uuids->uuidbytes);
+ free(uuids);
+}
+
+static void
+kcmreq_free(struct kcmreq *req)
+{
+ k5_buf_free(&req->reqbuf);
+ free(req->reply_mem);
+}
+
+/* Create a krb5_ccache structure. Always take ownership of io. */
+static krb5_error_code
+make_cache(const char *residual, struct kcmio *io, krb5_ccache *cache_out)
+{
+ krb5_ccache cache = NULL;
+ struct kcm_cache_data *data = NULL;
+ char *residual_copy = NULL;
+
+ *cache_out = NULL;
+ assert(io != NULL);
+
+ 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;
+ if (k5_cc_mutex_init(&data->lock) != 0)
+ goto oom;
+
+ data->residual = residual_copy;
+ data->io = io;
+ data->changetime = 0;
+ cache->ops = &krb5_kcm_ops;
+ cache->data = data;
+ cache->magic = KV5M_CCACHE;
+ *cache_out = cache;
+ return 0;
+
+oom:
+ free(cache);
+ free(data);
+ free(residual_copy);
+ kcmio_close(io);
+ return ENOMEM;
+}
+
+/* Lock cache's I/O structure and use it to call the KCM daemon. If modify is
+ * true, update the last change time. */
+static krb5_error_code
+cache_call(krb5_context context, krb5_ccache cache, struct kcmreq *req,
+ krb5_boolean modify)
+{
+ krb5_error_code ret;
+ struct kcm_cache_data *data = cache->data;
+
+ k5_cc_mutex_lock(context, &data->lock);
+ ret = kcmio_call(context, data->io, req);
+ if (modify && !ret)
+ data->changetime = time(NULL);
+ k5_cc_mutex_unlock(context, &data->lock);
+ return ret;
+}
+
+/* Try to propagate the KDC time offset from the cache to the krb5 context. */
+static void
+get_kdc_offset(krb5_context context, krb5_ccache cache)
+{
+ struct kcmreq req = EMPTY_KCMREQ;
+ int32_t time_offset;
+
+ kcmreq_init(&req, KCM_OP_GET_KDC_OFFSET, cache);
+ if (cache_call(context, cache, &req, FALSE) != 0)
+ goto cleanup;
+ time_offset = k5_input_get_uint32_be(&req.reply);
+ if (!req.reply.status)
+ goto cleanup;
+ context->os_context.time_offset = time_offset;
+ context->os_context.usec_offset = 0;
+ context->os_context.os_flags &= ~KRB5_OS_TOFFSET_TIME;
+ context->os_context.os_flags |= KRB5_OS_TOFFSET_VALID;
+
+cleanup:
+ kcmreq_free(&req);
+}
+
+/* Try to propagate the KDC offset from the krb5 context to the cache. */
+static void
+set_kdc_offset(krb5_context context, krb5_ccache cache)
+{
+ struct kcmreq req;
+
+ if (context->os_context.os_flags & KRB5_OS_TOFFSET_VALID) {
+ kcmreq_init(&req, KCM_OP_SET_KDC_OFFSET, cache);
+ kcmreq_put32(&req, context->os_context.time_offset);
+ (void)cache_call(context, cache, &req, TRUE);
+ kcmreq_free(&req);
+ }
+}
+
+static const char * KRB5_CALLCONV
+kcm_get_name(krb5_context context, krb5_ccache cache)
+{
+ return ((struct kcm_cache_data *)cache->data)->residual;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_resolve(krb5_context context, krb5_ccache *cache_out, const char *residual)
+{
+ krb5_error_code ret;
+ struct kcmreq req = EMPTY_KCMREQ;
+ struct kcmio *io = NULL;
+ const char *defname = NULL;
+
+ *cache_out = NULL;
+
+ ret = kcmio_connect(context, &io);
+ if (ret)
+ goto cleanup;
+
+ if (*residual == '\0') {
+ kcmreq_init(&req, KCM_OP_GET_DEFAULT_CACHE, NULL);
+ ret = kcmio_call(context, io, &req);
+ if (ret)
+ goto cleanup;
+ ret = kcmreq_get_name(&req, &defname);
+ if (ret)
+ goto cleanup;
+ residual = defname;
+ }
+
+ ret = make_cache(residual, io, cache_out);
+ io = NULL;
+
+cleanup:
+ kcmio_close(io);
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_gen_new(krb5_context context, krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req = EMPTY_KCMREQ;
+ struct kcmio *io = NULL;
+ const char *name;
+
+ *cache_out = NULL;
+
+ ret = kcmio_connect(context, &io);
+ if (ret)
+ goto cleanup;
+ kcmreq_init(&req, KCM_OP_GEN_NEW, NULL);
+ ret = kcmio_call(context, io, &req);
+ if (ret)
+ goto cleanup;
+ ret = kcmreq_get_name(&req, &name);
+ if (ret)
+ goto cleanup;
+ ret = make_cache(name, io, cache_out);
+ io = NULL;
+
+cleanup:
+ kcmreq_free(&req);
+ kcmio_close(io);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_initialize(krb5_context context, krb5_ccache cache, krb5_principal princ)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_INITIALIZE, cache);
+ k5_marshal_princ(&req.reqbuf, 4, princ);
+ ret = cache_call(context, cache, &req, TRUE);
+ kcmreq_free(&req);
+ set_kdc_offset(context, cache);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_close(krb5_context context, krb5_ccache cache)
+{
+ struct kcm_cache_data *data = cache->data;
+
+ k5_cc_mutex_destroy(&data->lock);
+ kcmio_close(data->io);
+ free(data->residual);
+ free(data);
+ free(cache);
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_destroy(krb5_context context, krb5_ccache cache)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_DESTROY, cache);
+ ret = cache_call(context, cache, &req, TRUE);
+ kcmreq_free(&req);
+ (void)kcm_close(context, cache);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_store(krb5_context context, krb5_ccache cache, krb5_creds *cred)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_STORE, cache);
+ k5_marshal_cred(&req.reqbuf, 4, cred);
+ ret = cache_call(context, cache, &req, TRUE);
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_retrieve(krb5_context context, krb5_ccache cache, krb5_flags flags,
+ krb5_creds *mcred, krb5_creds *cred_out)
+{
+ /* There is a KCM opcode for retrieving creds, but Heimdal's client doesn't
+ * use it. It causes the KCM daemon to actually make a TGS request. */
+ return k5_cc_retrieve_cred_default(context, cache, flags, mcred, cred_out);
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_get_princ(krb5_context context, krb5_ccache cache,
+ krb5_principal *princ_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_GET_PRINCIPAL, cache);
+ ret = cache_call(context, cache, &req, FALSE);
+ /* Heimdal KCM can respond with code 0 and no principal. */
+ if (!ret && req.reply.len == 0)
+ ret = KRB5_FCC_NOFILE;
+ if (!ret)
+ ret = k5_unmarshal_princ(req.reply.ptr, req.reply.len, 4, princ_out);
+ kcmreq_free(&req);
+ return map_invalid(ret);
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_start_seq_get(krb5_context context, krb5_ccache cache,
+ krb5_cc_cursor *cursor_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req = EMPTY_KCMREQ;
+ struct uuid_list *uuids;
+
+ *cursor_out = NULL;
+
+ get_kdc_offset(context, cache);
+
+ kcmreq_init(&req, KCM_OP_GET_CRED_UUID_LIST, cache);
+ ret = cache_call(context, cache, &req, FALSE);
+ if (ret)
+ goto cleanup;
+ ret = kcmreq_get_uuid_list(&req, &uuids);
+ if (ret)
+ goto cleanup;
+ *cursor_out = (krb5_cc_cursor)uuids;
+
+cleanup:
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_next_cred(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor,
+ krb5_creds *cred_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+ struct uuid_list *uuids = (struct uuid_list *)*cursor;
+
+ memset(cred_out, 0, sizeof(*cred_out));
+
+ if (uuids->pos >= uuids->count)
+ return KRB5_CC_END;
+
+ kcmreq_init(&req, KCM_OP_GET_CRED_BY_UUID, cache);
+ k5_buf_add_len(&req.reqbuf, uuids->uuidbytes + (uuids->pos * KCM_UUID_LEN),
+ KCM_UUID_LEN);
+ uuids->pos++;
+ ret = cache_call(context, cache, &req, FALSE);
+ if (!ret)
+ ret = k5_unmarshal_cred(req.reply.ptr, req.reply.len, 4, cred_out);
+ kcmreq_free(&req);
+ return map_invalid(ret);
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_end_seq_get(krb5_context context, krb5_ccache cache,
+ krb5_cc_cursor *cursor)
+{
+ free_uuid_list((struct uuid_list *)*cursor);
+ *cursor = NULL;
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_remove_cred(krb5_context context, krb5_ccache cache, krb5_flags flags,
+ krb5_creds *mcred)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_REMOVE_CRED, cache);
+ kcmreq_put32(&req, flags);
+ k5_marshal_mcred(&req.reqbuf, mcred);
+ ret = cache_call(context, cache, &req, TRUE);
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_set_flags(krb5_context context, krb5_ccache cache, krb5_flags flags)
+{
+ /* We don't currently care about any flags for this type. */
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_get_flags(krb5_context context, krb5_ccache cache, krb5_flags *flags_out)
+{
+ /* We don't currently have any operational flags for this type. */
+ *flags_out = 0;
+ return 0;
+}
+
+/* Construct a per-type cursor, always taking ownership of io and uuids. */
+static krb5_error_code
+make_ptcursor(struct uuid_list *uuids, struct kcmio *io,
+ krb5_cc_ptcursor *cursor_out)
+{
+ krb5_cc_ptcursor cursor = NULL;
+ struct kcm_ptcursor *data = NULL;
+
+ *cursor_out = NULL;
+
+ cursor = malloc(sizeof(*cursor));
+ if (cursor == NULL)
+ goto oom;
+ data = malloc(sizeof(*data));
+ if (data == NULL)
+ goto oom;
+
+ data->uuids = uuids;
+ data->io = io;
+ cursor->ops = &krb5_kcm_ops;
+ cursor->data = data;
+ *cursor_out = cursor;
+ return 0;
+
+oom:
+ kcmio_close(io);
+ free_uuid_list(uuids);
+ free(data);
+ free(cursor);
+ return ENOMEM;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_ptcursor_new(krb5_context context, krb5_cc_ptcursor *cursor_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req = EMPTY_KCMREQ;
+ struct kcmio *io = NULL;
+ struct uuid_list *uuids;
+ const char *defname;
+
+ *cursor_out = NULL;
+
+ /* Don't try to use KCM for the cache collection unless the default cache
+ * name has the KCM type. */
+ defname = krb5_cc_default_name(context);
+ if (defname == NULL || strncmp(defname, "KCM:", 4) != 0)
+ return make_ptcursor(NULL, NULL, cursor_out);
+
+ ret = kcmio_connect(context, &io);
+ if (ret)
+ goto cleanup;
+
+ kcmreq_init(&req, KCM_OP_GET_CACHE_UUID_LIST, NULL);
+ ret = kcmio_call(context, io, &req);
+ if (ret == KRB5_FCC_NOFILE) {
+ /* There are no accessible caches; return an empty cursor. */
+ ret = make_ptcursor(NULL, NULL, cursor_out);
+ goto cleanup;
+ }
+ if (ret)
+ goto cleanup;
+
+ ret = kcmreq_get_uuid_list(&req, &uuids);
+ if (ret)
+ goto cleanup;
+
+ ret = make_ptcursor(uuids, io, cursor_out);
+ io = NULL;
+
+cleanup:
+ kcmio_close(io);
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_ptcursor_next(krb5_context context, krb5_cc_ptcursor cursor,
+ krb5_ccache *cache_out)
+{
+ krb5_error_code ret;
+ struct kcmreq req = EMPTY_KCMREQ;
+ struct kcmio *io;
+ struct kcm_ptcursor *data = cursor->data;
+ struct uuid_list *uuids = data->uuids;
+ const char *name;
+
+ *cache_out = NULL;
+
+ if (uuids == NULL || uuids->pos >= uuids->count)
+ return 0;
+
+ kcmreq_init(&req, KCM_OP_GET_CACHE_BY_UUID, NULL);
+ k5_buf_add_len(&req.reqbuf, uuids->uuidbytes + (uuids->pos * KCM_UUID_LEN),
+ KCM_UUID_LEN);
+ uuids->pos++;
+ ret = kcmio_call(context, data->io, &req);
+ if (ret)
+ goto cleanup;
+
+ ret = kcmreq_get_name(&req, &name);
+ if (ret)
+ goto cleanup;
+
+ ret = kcmio_connect(context, &io);
+ if (ret)
+ goto cleanup;
+ ret = make_cache(name, io, cache_out);
+
+cleanup:
+ kcmreq_free(&req);
+ return ret;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_ptcursor_free(krb5_context context, krb5_cc_ptcursor *cursor)
+{
+ struct kcm_ptcursor *data = (*cursor)->data;
+
+ free_uuid_list(data->uuids);
+ kcmio_close(data->io);
+ free(data);
+ free(*cursor);
+ *cursor = NULL;
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_lastchange(krb5_context context, krb5_ccache cache,
+ krb5_timestamp *time_out)
+{
+ struct kcm_cache_data *data = cache->data;
+
+ /*
+ * KCM has no support for retrieving the last change time. Return the time
+ * of the last change made through this handle, which isn't very useful,
+ * but is the best we can do for now.
+ */
+ k5_cc_mutex_lock(context, &data->lock);
+ *time_out = data->changetime;
+ k5_cc_mutex_unlock(context, &data->lock);
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_lock(krb5_context context, krb5_ccache cache)
+{
+ k5_cc_mutex_lock(context, &((struct kcm_cache_data *)cache->data)->lock);
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_unlock(krb5_context context, krb5_ccache cache)
+{
+ k5_cc_mutex_unlock(context, &((struct kcm_cache_data *)cache->data)->lock);
+ return 0;
+}
+
+static krb5_error_code KRB5_CALLCONV
+kcm_switch_to(krb5_context context, krb5_ccache cache)
+{
+ krb5_error_code ret;
+ struct kcmreq req;
+
+ kcmreq_init(&req, KCM_OP_SET_DEFAULT_CACHE, cache);
+ ret = cache_call(context, cache, &req, FALSE);
+ kcmreq_free(&req);
+ return ret;
+}
+
+const krb5_cc_ops krb5_kcm_ops = {
+ 0,
+ "KCM",
+ kcm_get_name,
+ kcm_resolve,
+ kcm_gen_new,
+ kcm_initialize,
+ kcm_destroy,
+ kcm_close,
+ kcm_store,
+ kcm_retrieve,
+ kcm_get_princ,
+ kcm_start_seq_get,
+ kcm_next_cred,
+ kcm_end_seq_get,
+ kcm_remove_cred,
+ kcm_set_flags,
+ kcm_get_flags,
+ kcm_ptcursor_new,
+ kcm_ptcursor_next,
+ kcm_ptcursor_free,
+ NULL, /* move */
+ kcm_lastchange,
+ NULL, /* wasdefault */
+ kcm_lock,
+ kcm_unlock,
+ kcm_switch_to,
+};
+
+#endif /* not _WIN32 */