/*
SSSD
KCM Server - the KCM server request and reply parsing and dispatching
Copyright (C) Red Hat, 2016
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
#include
#include "config.h"
#include "util/util.h"
#include "responder/common/responder.h"
#include "responder/kcm/kcmsrv_pvt.h"
#include "responder/kcm/kcmsrv_ops.h"
/* 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
/* 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 */
struct kcm_iovec v_msg;
};
static uint32_t kcm_input_get_payload_len(struct kcm_iovec *v)
{
size_t lc = 0;
uint32_t len_be = 0;
/* The first 4 bytes before the payload is message length */
SAFEALIGN_COPY_UINT32_CHECK(&len_be, v->kiov_base, v->kiov_len, &lc);
return be32toh(len_be);
}
static errno_t kcm_input_parse(struct kcm_reqbuf *reqbuf,
struct kcm_op_io *op_io)
{
size_t mc = 0;
uint16_t opcode_be = 0;
uint32_t msglen;
uint8_t proto_maj = 0;
uint8_t proto_min = 0;
msglen = kcm_input_get_payload_len(&reqbuf->v_len);
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 minor 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);
op_io->op = kcm_get_opt(be16toh(opcode_be));
if (op_io->op == NULL) {
DEBUG(SSSDBG_MINOR_FAILURE,
"Did not find a KCM operation handler for the requested opcode\n");
return ERR_KCM_OP_NOT_IMPLEMENTED;
}
/* The operation only receives the payload, not the opcode or the protocol info */
op_io->request.data = reqbuf->v_msg.kiov_base + mc;
op_io->request.length = reqbuf->v_msg.nprocessed - mc;
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;
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);
DEBUG(SSSDBG_TRACE_LIBS, "Sent reply with error %d\n", ret);
return EOK;
}
/* retcode is 0 if the operation at least ran, non-zero if there
* was some kind of internal KCM error, like input couldn't be parsed
*/
static errno_t kcm_output_construct(TALLOC_CTX *mem_ctx,
struct kcm_op_io *op_io,
struct kcm_repbuf *repbuf)
{
uint8_t *rep;
size_t replen;
size_t c;
replen = sss_iobuf_get_len(op_io->reply);
if (replen > KCM_PACKET_MAX_SIZE) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Reply exceeds the KCM protocol limit, aborting\n");
return E2BIG;
}
DEBUG(SSSDBG_TRACE_LIBS,
"Sending a reply with %zu bytes of payload\n", replen);
c = 0;
SAFEALIGN_SETMEM_UINT32(repbuf->lenbuf, htobe32(replen), &c);
c = 0;
SAFEALIGN_SETMEM_UINT32(repbuf->rcbuf, 0, &c);
if (replen > 0) {
rep = talloc_array(mem_ctx, uint8_t, replen);
if (rep == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Failed to allocate memory for the message\n");
return ENOMEM;
}
c = 0;
SAFEALIGN_MEMCPY_CHECK(rep,
sss_iobuf_get_data(op_io->reply),
replen,
replen,
&c);
/* Set the buffer and its length to send to KCM client */
repbuf->v_msg.kiov_base = rep;
repbuf->v_msg.kiov_len = replen;
}
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(retcode == ERR_KCM_OP_NOT_IMPLEMENTED ?
SSSDBG_MINOR_FAILURE : SSSDBG_OP_FAILURE,
"KCM operation returns failure [%d]: %s\n",
retcode, sss_strerror(retcode));
kerr = sss2krb5_error(retcode);
ret = kcm_failbuf_construct(kerr, repbuf);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot construct the reply buffer, terminating client\n");
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 void kcm_send_reply(struct kcm_req_ctx *req_ctx)
{
struct cli_ctx *cctx;
errno_t ret;
DEBUG(SSSDBG_TRACE_INTERNAL, "Sending a reply\n");
cctx = req_ctx->cctx;
ret = kcm_output_construct(req_ctx, &req_ctx->op_io, &req_ctx->repbuf);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot construct the reply buffer, terminating client\n");
kcm_reply_error(cctx, ret, &req_ctx->repbuf);
return;
}
TEVENT_FD_WRITEABLE(cctx->cfde);
}
static void kcm_cmd_request_done(struct tevent_req *req);
static errno_t kcm_cmd_dispatch(struct kcm_ctx *kctx,
struct kcm_req_ctx *req_ctx)
{
struct tevent_req *req;
struct cli_ctx *cctx;
struct kcm_conn_data *conn_data;
cctx = req_ctx->cctx;
conn_data = talloc_get_type(cctx->state_ctx, struct kcm_conn_data);
req = kcm_cmd_send(req_ctx,
cctx->ev,
kctx->qctx,
req_ctx->kctx->kcm_data,
conn_data,
req_ctx->cctx->creds,
&req_ctx->op_io.request,
req_ctx->op_io.op);
if (req == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE, "Failed to schedule KCM operation.\n");
return ENOMEM;
}
tevent_req_set_callback(req, kcm_cmd_request_done, req_ctx);
return EOK;
}
static void kcm_cmd_request_done(struct tevent_req *req)
{
struct kcm_req_ctx *req_ctx;
errno_t ret;
req_ctx = tevent_req_callback_data(req, struct kcm_req_ctx);
ret = kcm_cmd_recv(req_ctx, req,
&req_ctx->op_io.reply);
talloc_free(req);
if (ret != EOK) {
if (ret == ERR_KCM_OP_NOT_IMPLEMENTED) {
DEBUG(SSSDBG_MINOR_FAILURE, "%s\n", sss_strerror(ret));
} else {
DEBUG(SSSDBG_OP_FAILURE,
"KCM operation failed [%d]: %s\n", ret, sss_strerror(ret));
}
kcm_reply_error(req_ctx->cctx, ret, &req_ctx->repbuf);
return;
}
kcm_send_reply(req_ctx);
}
static errno_t kcm_recv_data(TALLOC_CTX *mem_ctx,
int fd,
struct kcm_reqbuf *reqbuf)
{
uint8_t *msg;
uint32_t msglen;
errno_t ret;
ret = kcm_read_iovec(fd, &reqbuf->v_len);
if (ret != EOK) {
/* Not all errors are fatal, hence we don't print DEBUG messages
* here, but in the caller
*/
return ret;
}
msglen = kcm_input_get_payload_len(&reqbuf->v_len);
if (msglen > KCM_PACKET_MAX_SIZE) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Request exceeds the KCM protocol limit, aborting\n");
return E2BIG;
}
msg = talloc_array(mem_ctx, uint8_t, msglen);
if (msg == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Failed to allocate memory for the message\n");
return ENOMEM;
}
/* Set the buffer and its expected len to receive the data */
reqbuf->v_msg.kiov_base = msg;
reqbuf->v_msg.kiov_len = msglen;
ret = kcm_read_iovec(fd, &reqbuf->v_msg);
if (ret != EOK) {
/* Not all errors are fatal, hence we don't print DEBUG messages
* here, but in the caller
*/
return ret;
}
return EOK;
}
/* Mind that kcm_new_req() does not take a mem_ctx argument on purpose as we
* really want the cctx to be the memory context here so that if the client
* disconnects, the request goes away. */
static struct kcm_req_ctx *kcm_new_req(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->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->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->protocol_ctx, struct kcm_req_ctx);
if (req == NULL) {
/* A new request comes in, setup data structures. */
req = kcm_new_req(cctx, kctx);
if (req == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Cannot set up client connection\n");
talloc_free(cctx);
return;
}
cctx->protocol_ctx = req;
}
/* Shared data between requests that originates in the same connection. */
if (cctx->state_ctx == NULL) {
cctx->state_ctx = talloc_zero(cctx, struct kcm_conn_data);
if (cctx->state_ctx == NULL) {
DEBUG(SSSDBG_CRIT_FAILURE, "Cannot set up client state\n");
talloc_free(cctx);
return;
}
}
ret = kcm_recv_data(req, 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));
talloc_free(cctx);
return;
}
/* do not read anymore, client is done sending */
TEVENT_FD_NOT_READABLE(cctx->cfde);
ret = kcm_cmd_dispatch(kctx, req);
if (ret != EOK) {
DEBUG(SSSDBG_FATAL_FAILURE,
"Failed to dispatch KCM operation [%d]: %s\n",
ret, sss_strerror(ret));
/* Fail with reply */
kcm_reply_error(cctx, ret, &req->repbuf);
return;
}
/* Dispatched request resumes in kcm_cmd_request_done */
return;
}
static int kcm_send_data(struct cli_ctx *cctx)
{
struct kcm_req_ctx *req;
errno_t ret;
req = talloc_get_type(cctx->protocol_ctx, struct kcm_req_ctx);
ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_len);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Failed to write the length iovec [%d]: %s\n",
ret, sss_strerror(ret));
return ret;
}
ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_rc);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Failed to write the retcode iovec [%d]: %s\n",
ret, sss_strerror(ret));
return ret;
}
ret = kcm_write_iovec(cctx->cfd, &req->repbuf.v_msg);
if (ret != EOK) {
DEBUG(SSSDBG_CRIT_FAILURE,
"Failed to write the msg iovec [%d]: %s\n",
ret, sss_strerror(ret));
return ret;
}
return EOK;
}
static void kcm_send(struct cli_ctx *cctx)
{
errno_t ret;
ret = kcm_send_data(cctx);
if (ret == EAGAIN) {
DEBUG(SSSDBG_TRACE_ALL, "Sending data again..\n");
return;
} else if (ret != EOK) {
DEBUG(SSSDBG_FATAL_FAILURE, "Failed to send data, aborting client!\n");
talloc_free(cctx);
return;
}
DEBUG(SSSDBG_TRACE_INTERNAL, "All data sent!\n");
TEVENT_FD_NOT_WRITEABLE(cctx->cfde);
TEVENT_FD_READABLE(cctx->cfde);
talloc_zfree(cctx->protocol_ctx);
return;
}
static void kcm_fd_handler(struct tevent_context *ev,
struct tevent_fd *fde,
uint16_t flags, void *ptr)
{
sss_client_fd_handler(ptr, kcm_recv, kcm_send, flags);
}
int kcm_connection_setup(struct cli_ctx *cctx)
{
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_FCC_INTERNAL;
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)
{
return NULL;
}