Blob Blame History Raw
From aface2604c53db717299ac3dfe798dfd0c540f99 Mon Sep 17 00:00:00 2001
From: Jakub Hrozek <jhrozek@redhat.com>
Date: Fri, 23 Sep 2016 14:00:10 +0200
Subject: [PATCH 23/36] KCM: request parsing and sending a reply
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implements parsing the KCM client request into per-client buffers and
sending a response for both the failure case and for success.

The protocol is documented at:
    http://k5wiki.kerberos.org/wiki/Projects/KCM_client

Several places don't use the sss_iobuf structure, because they don't
parse variable-length data from the buffer and it's much more efficient
to just allocate the needed request and reply structure on the stack.

Reviewed-by: Michal Židek <mzidek@redhat.com>
Reviewed-by: Simo Sorce <simo@redhat.com>
---
 src/responder/kcm/kcmsrv_cmd.c | 467 ++++++++++++++++++++++++++++++++++++++++-
 src/responder/kcm/kcmsrv_pvt.h |  21 +-
 2 files changed, 474 insertions(+), 14 deletions(-)

diff --git a/src/responder/kcm/kcmsrv_cmd.c b/src/responder/kcm/kcmsrv_cmd.c
index e9a03cbd41169c93e00b0630dc1e05e205881ec9..cbf70353730d8a4e03d8f75c97395f4ef007e77f 100644
--- a/src/responder/kcm/kcmsrv_cmd.c
+++ b/src/responder/kcm/kcmsrv_cmd.c
@@ -19,14 +19,430 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+#include <krb5/krb5.h>
+
 #include "config.h"
 #include "util/util.h"
+#include "util/sss_iobuf.h"
 #include "responder/common/responder.h"
+#include "responder/kcm/kcmsrv_pvt.h"
+#include "responder/kcm/kcm.h"
 
-struct kcm_proto_ctx {
-    void *unused;
+/* The first four bytes of a message is always the size */
+#define KCM_MSG_LEN_SIZE 4
+
+/* The return code is 32bits */
+#define KCM_RETCODE_SIZE 4
+
+/* The maximum length of a request or reply as defined by the RPC
+ * protocol. This is the same constant size as MIT KRB5 uses
+ */
+#define KCM_PACKET_MAX_SIZE 2048
+
+/* KCM operation, its raw input and raw output and result */
+struct kcm_op_io {
+    struct kcm_op *op;
+    struct kcm_data request;
+    struct sss_iobuf *reply;
+};
+
+/**
+ * KCM IO-vector operations
+ */
+struct kcm_iovec {
+    /* We don't use iovec b/c void pointers don't allow for
+     * pointer arithmetics and it's convenient to keep track
+     * of processed bytes
+     */
+    uint8_t *kiov_base;
+    size_t kiov_len;
+    size_t nprocessed;
+};
+
+static errno_t kcm_iovec_op(int fd, struct kcm_iovec *kiov, bool do_read)
+{
+    ssize_t len;
+    struct iovec iov[1];
+
+    iov[0].iov_base = kiov->kiov_base + kiov->nprocessed;
+    iov[0].iov_len = kiov->kiov_len - kiov->nprocessed;
+    if (iov[0].iov_len == 0) {
+        /* This iovec is full (read) or depleted (write), proceed to the next one */
+        return EOK;
+    }
+
+    if (do_read) {
+        len = readv(fd, iov, 1);
+    } else {
+        len = writev(fd, iov, 1);
+    }
+
+    if (len == -1) {
+        if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
+            return EAGAIN;
+        } else {
+            return errno;
+        }
+    }
+
+    if (len == 0) {
+        /* Read event on fd that doesn't yield data? error */
+        return ENODATA;
+    }
+
+    /* Decrease the amount of available free space in the iovec */
+    kiov->nprocessed += len;
+    return EOK;
+}
+
+static errno_t kcm_read_iovec(int fd, struct kcm_iovec *kiov)
+{
+    return kcm_iovec_op(fd, kiov, true);
+}
+
+static errno_t kcm_write_iovec(int fd, struct kcm_iovec *kiov)
+{
+    return kcm_iovec_op(fd, kiov, false);
+}
+
+/**
+ * Parsing KCM input
+ *
+ * The request is received as two IO vectors:
+ *
+ * first iovec:
+ *  length                      32-bit big-endian integer
+ *
+ * second iovec:
+ *  major protocol number       8-bit big-endian integer
+ *  minor protocol number       8-bit big-endian integer
+ *  opcode                      16-bit big-endian integer
+ *  message payload             buffer
+ */
+struct kcm_reqbuf {
+    uint8_t lenbuf[KCM_MSG_LEN_SIZE];
+    struct kcm_iovec v_len;
+
+    /* Includes the major, minor versions etc */
+    uint8_t msgbuf[KCM_PACKET_MAX_SIZE];
+    struct kcm_iovec v_msg;
+};
+
+static errno_t kcm_input_parse(struct kcm_reqbuf *reqbuf,
+                               struct kcm_op_io *op_io)
+{
+    size_t lc = 0;
+    size_t mc = 0;
+    uint16_t opcode = 0;
+    uint16_t opcode_be = 0;
+    uint32_t len_be = 0;
+    uint32_t msglen;
+    uint8_t proto_maj = 0;
+    uint8_t proto_min = 0;
+
+    /* The first 4 bytes before the payload is message length */
+    SAFEALIGN_COPY_UINT32_CHECK(&len_be,
+                                reqbuf->v_len.kiov_base,
+                                reqbuf->v_len.kiov_len,
+                                &lc);
+    msglen = be32toh(len_be);
+    DEBUG(SSSDBG_TRACE_LIBS,
+          "Received message with length %"PRIu32"\n", msglen);
+
+    if (msglen == 0) {
+        DEBUG(SSSDBG_CRIT_FAILURE, "Illegal zero-length message\n");
+        return EBADMSG;
+    }
+
+    if (msglen != reqbuf->v_msg.nprocessed) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "Sender claims the message is %"PRIu32" bytes, "
+              "but received %zu\n",
+              msglen, reqbuf->v_msg.nprocessed);
+        return EBADMSG;
+    }
+
+    /* First 16 bits are 8 bit major and 8bit major protocol version */
+    SAFEALIGN_COPY_UINT8_CHECK(&proto_maj,
+                               reqbuf->v_msg.kiov_base + mc,
+                               reqbuf->v_msg.kiov_len,
+                               &mc);
+    SAFEALIGN_COPY_UINT8_CHECK(&proto_min,
+                               reqbuf->v_msg.kiov_base + mc,
+                               reqbuf->v_msg.kiov_len,
+                               &mc);
+
+    if (proto_maj != KCM_PROTOCOL_VERSION_MAJOR) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "Expected major version %d, got %"PRIu16"\n",
+              KCM_PROTOCOL_VERSION_MAJOR, (uint16_t) proto_maj);
+        return ERR_KCM_MALFORMED_IN_PKT;
+    }
+
+    if (proto_min != KCM_PROTOCOL_VERSION_MINOR) {
+        DEBUG(SSSDBG_CRIT_FAILURE,
+              "Expected minor version %d, got %"PRIu16"\n",
+              KCM_PROTOCOL_VERSION_MINOR, (uint16_t) proto_maj);
+        return ERR_KCM_MALFORMED_IN_PKT;
+    }
+
+    SAFEALIGN_COPY_UINT16_CHECK(&opcode_be,
+                                reqbuf->v_msg.kiov_base + mc,
+                                reqbuf->v_msg.kiov_len,
+                                &mc);
+
+    opcode = be16toh(opcode_be);
+    DEBUG(SSSDBG_TRACE_LIBS, "Received operation code %"PRIu16"\n", opcode);
+
+    return EOK;
+}
+
+/**
+ * Constructing a reply for failure and success
+ *
+ * The reply consists of three IO vectors:
+ * 1) length iovec:
+ *  length:     32-bit big-endian
+ *
+ * 2) return code iovec:
+ *  retcode:    32-bit big-endian. Non-zero on failure in the KCM server,
+ *              zero if the KCM operation ran (even if the operation itself
+ *              failed)
+ *
+ * 3) reply iovec
+ *  message:    buffer, first 32-bits of the buffer is the return code of
+ *              the KCM operation, the rest depends on the operation itself.
+ *              The buffer's length is specified by the first integer in the
+ *              reply (very intuitive, right?)
+ *
+ *  The client always reads the length and return code iovectors. However, the
+ *  client reads the reply iovec only if retcode is 0 in the return code iovector
+ *  (see kcmio_unix_socket_read() in the MIT tree)
+ */
+struct kcm_repbuf {
+    uint8_t lenbuf[KCM_MSG_LEN_SIZE];
+    struct kcm_iovec v_len;
+
+    uint8_t rcbuf[KCM_RETCODE_SIZE];
+    struct kcm_iovec v_rc;
+
+    uint8_t msgbuf[KCM_PACKET_MAX_SIZE];
+    struct kcm_iovec v_msg;
+};
+
+static errno_t kcm_failbuf_construct(errno_t ret,
+                                     struct kcm_repbuf *repbuf)
+{
+    size_t c;
+
+    c = 0;
+    SAFEALIGN_SETMEM_UINT32(repbuf->lenbuf, 0, &c);
+    c = 0;
+    SAFEALIGN_SETMEM_UINT32(repbuf->rcbuf, htobe32(ret), &c);
+
+    return EOK;
+}
+
+/**
+ * Construct a reply buffer and send it to the KCM client
+ */
+static void kcm_reply_error(struct cli_ctx *cctx,
+                            errno_t retcode,
+                            struct kcm_repbuf *repbuf)
+{
+    errno_t ret;
+    krb5_error_code kerr;
+
+    DEBUG(SSSDBG_OP_FAILURE,
+          "KCM operation returs failure [%d]: %s\n",
+          retcode, sss_strerror(retcode));
+    kerr = sss2krb5_error(retcode);
+
+    ret = kcm_failbuf_construct(kerr, repbuf);
+    if (ret != EOK) {
+        /* If we can't construct the reply buffer, just terminate the client */
+        talloc_free(cctx);
+        return;
+    }
+
+    TEVENT_FD_WRITEABLE(cctx->cfde);
+}
+
+/**
+ * Request-reply dispatcher
+ */
+struct kcm_req_ctx {
+    /* client context owns per-client buffers including this one */
+    struct cli_ctx *cctx;
+
+    /* raw IO buffers */
+    struct kcm_reqbuf reqbuf;
+    struct kcm_repbuf repbuf;
+
+    /* long-lived responder structures */
+    struct kcm_ctx *kctx;
+
+    struct kcm_op_io op_io;
 };
 
+static errno_t kcm_recv_data(int fd, struct kcm_reqbuf *reqbuf)
+{
+    errno_t ret;
+
+    ret = kcm_read_iovec(fd, &reqbuf->v_len);
+    if (ret != EOK) {
+        return ret;
+    }
+
+    ret = kcm_read_iovec(fd, &reqbuf->v_msg);
+    if (ret != EOK) {
+        return ret;
+    }
+
+    return EOK;
+}
+
+static struct kcm_req_ctx *kcm_new_req(TALLOC_CTX *mem_ctx,
+                                       struct cli_ctx *cctx,
+                                       struct kcm_ctx *kctx)
+{
+    struct kcm_req_ctx *req;
+
+    req = talloc_zero(cctx, struct kcm_req_ctx);
+    if (req == NULL) {
+        return NULL;
+    }
+
+    req->reqbuf.v_len.kiov_base = req->reqbuf.lenbuf;
+    req->reqbuf.v_len.kiov_len = KCM_MSG_LEN_SIZE;
+
+    req->reqbuf.v_msg.kiov_base = req->reqbuf.msgbuf;
+    req->reqbuf.v_msg.kiov_len = KCM_PACKET_MAX_SIZE;
+
+    req->repbuf.v_len.kiov_base = req->repbuf.lenbuf;
+    req->repbuf.v_len.kiov_len = KCM_MSG_LEN_SIZE;
+
+    req->repbuf.v_rc.kiov_base = req->repbuf.rcbuf;
+    req->repbuf.v_rc.kiov_len = KCM_RETCODE_SIZE;
+
+    req->repbuf.v_msg.kiov_base = req->repbuf.msgbuf;
+    /* Length of the msg iobuf will be adjusted later, so far use the full
+     * length so that constructing the reply can use that capacity
+     */
+    req->repbuf.v_msg.kiov_len = KCM_PACKET_MAX_SIZE;
+
+    req->cctx = cctx;
+    req->kctx = kctx;
+
+    return req;
+}
+
+static void kcm_recv(struct cli_ctx *cctx)
+{
+    struct kcm_req_ctx *req;
+    struct kcm_ctx *kctx;
+    int ret;
+
+    kctx = talloc_get_type(cctx->rctx->pvt_ctx, struct kcm_ctx);
+    req = talloc_get_type(cctx->state_ctx, struct kcm_req_ctx);
+    if (req == NULL) {
+        /* A new request comes in, setup data structures */
+        req = kcm_new_req(cctx, cctx, kctx);
+        if (req == NULL) {
+            DEBUG(SSSDBG_CRIT_FAILURE,
+                  "Cannot set up client connection\n");
+            talloc_free(cctx);
+            return;
+        }
+
+        cctx->state_ctx = req;
+    }
+
+    ret = kcm_recv_data(cctx->cfd, &req->reqbuf);
+    switch (ret) {
+    case ENODATA:
+        DEBUG(SSSDBG_TRACE_ALL, "Client closed connection.\n");
+        talloc_free(cctx);
+        return;
+    case EAGAIN:
+        DEBUG(SSSDBG_TRACE_ALL, "Retry later\n");
+        return;
+    case EOK:
+        /* all fine */
+        break;
+    default:
+        DEBUG(SSSDBG_FATAL_FAILURE,
+              "Failed to receive data (%d, %s), aborting client\n",
+              ret, sss_strerror(ret));
+        talloc_free(cctx);
+        return;
+    }
+
+    ret = kcm_input_parse(&req->reqbuf, &req->op_io);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_FATAL_FAILURE,
+            "Failed to parse data (%d, %s), aborting client\n",
+            ret, sss_strerror(ret));
+        goto fail;
+    }
+
+    /* do not read anymore, client is done sending */
+    TEVENT_FD_NOT_READABLE(cctx->cfde);
+
+    kcm_reply_error(cctx, ret, &req->repbuf);
+    return;
+
+fail:
+    /* Fail with reply */
+    kcm_reply_error(cctx, ret, &req->repbuf);
+}
+
+static int kcm_send_data(struct cli_ctx *cctx)
+{
+    struct kcm_req_ctx *req;
+    errno_t ret;
+
+    req = talloc_get_type(cctx->state_ctx, struct kcm_req_ctx);
+
+    ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_len);
+    if (ret != EOK) {
+        return ret;
+    }
+
+    ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_rc);
+    if (ret != EOK) {
+        return ret;
+    }
+
+    ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_msg);
+    if (ret != EOK) {
+        return ret;
+    }
+
+    return EOK;
+}
+
+static void kcm_send(struct cli_ctx *cctx)
+{
+    errno_t ret;
+
+    ret = kcm_send_data(cctx);
+    if (ret == EAGAIN) {
+        /* not all data was sent, loop again */
+        return;
+    } else if (ret != EOK) {
+        DEBUG(SSSDBG_FATAL_FAILURE, "Failed to send data, aborting client!\n");
+        talloc_free(cctx);
+        return;
+    }
+
+    /* ok all sent */
+    TEVENT_FD_NOT_WRITEABLE(cctx->cfde);
+    TEVENT_FD_READABLE(cctx->cfde);
+    talloc_zfree(cctx->state_ctx);
+    return;
+}
+
 static void kcm_fd_handler(struct tevent_context *ev,
                            struct tevent_fd *fde,
                            uint16_t flags, void *ptr)
@@ -39,25 +455,54 @@ static void kcm_fd_handler(struct tevent_context *ev,
     if (ret != EOK) {
         DEBUG(SSSDBG_CRIT_FAILURE,
               "Could not create idle timer for client. "
-               "This connection may not auto-terminate\n");
+              "This connection may not auto-terminate\n");
         /* Non-fatal, continue */
     }
+
+    if (flags & TEVENT_FD_READ) {
+        kcm_recv(cctx);
+        return;
+    }
+    if (flags & TEVENT_FD_WRITE) {
+        kcm_send(cctx);
+        return;
+    }
 }
 
 int kcm_connection_setup(struct cli_ctx *cctx)
 {
-    struct kcm_proto_ctx *protocol_ctx;
-
-    protocol_ctx = talloc_zero(cctx, struct kcm_proto_ctx);
-    if (protocol_ctx == NULL) {
-        return ENOMEM;
-    }
-
-    cctx->protocol_ctx = protocol_ctx;
     cctx->cfd_handler = kcm_fd_handler;
     return EOK;
 }
 
+krb5_error_code sss2krb5_error(errno_t err)
+{
+    switch (err) {
+    case EOK:
+        return 0;
+    case ENOMEM:
+        return KRB5_CC_NOMEM;
+    case EACCES:
+        return KRB5_FCC_PERM;
+    case ERR_KCM_OP_NOT_IMPLEMENTED:
+        return KRB5_CC_NOSUPP;
+    case ERR_WRONG_NAME_FORMAT:
+        return KRB5_CC_BADNAME;
+    case ERR_NO_MATCHING_CREDS:
+        return KRB5_FCC_NOFILE;
+    case ERR_NO_CREDS:
+        return KRB5_CC_NOTFOUND;
+    case ERR_KCM_CC_END:
+        return KRB5_CC_END;
+    case ERR_KCM_MALFORMED_IN_PKT:
+    case EINVAL:
+    case EIO:
+        return KRB5_CC_IO;
+    }
+
+    return KRB5_FCC_INTERNAL;
+}
+
 /* Dummy, not used here but required to link to other responder files */
 struct cli_protocol_version *register_cli_protocol_version(void)
 {
diff --git a/src/responder/kcm/kcmsrv_pvt.h b/src/responder/kcm/kcmsrv_pvt.h
index a7c9d062c17f09986d894064176c3a461d396ac0..fd1fd9fa32d59a323d465def68999f24f84e3923 100644
--- a/src/responder/kcm/kcmsrv_pvt.h
+++ b/src/responder/kcm/kcmsrv_pvt.h
@@ -27,13 +27,20 @@
 #include <sys/types.h>
 #include "responder/common/responder.h"
 
-/* KCM IO structure */
+/*
+ * KCM IO structure
+ *
+ * In theory we cold use sss_iobuf there, but since iobuf was
+ * made opaque, this allows it to allocate the structures on
+ * the stack in one go.
+ * */
 struct kcm_data {
     uint8_t *data;
     size_t length;
 };
 
-/* To avoid leaking the sssd-specific responder data to other
+/*
+ * To avoid leaking the sssd-specific responder data to other
  * modules, the ccache databases and other KCM specific data
  * are kept separately
  */
@@ -41,7 +48,8 @@ struct kcm_resp_ctx {
     krb5_context k5c;
 };
 
-/* responder context that contains both the responder data,
+/*
+ * responder context that contains both the responder data,
  * like the ccaches and the sssd-specific stuff like the
  * generic responder ctx
  */
@@ -55,4 +63,11 @@ struct kcm_ctx {
 
 int kcm_connection_setup(struct cli_ctx *cctx);
 
+/*
+ * Internally in SSSD-KCM we use SSSD-internal error codes so that we
+ * can always the same sss_strerror() functions to format the errors
+ * nicely, but the client expects libkrb5 error codes.
+ */
+krb5_error_code sss2krb5_error(errno_t err);
+
 #endif /* __KCMSRV_PVT_H__ */
-- 
2.9.3