root/usr/src/cmd/smbsrv/smbd/smbd_authsvc.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2017 Nexenta Systems, Inc.  All rights reserved.
 * Copyright 2022-2023 RackTop Systems, Inc.
 */

/*
 * SMB authentication service
 *
 * This service listens on a local AF_UNIX socket, spawning a
 * thread to service each connection.  The client-side of such
 * connections is the in-kernel SMB service, with an open and
 * connect done in the SMB session setup handler.
 */

#include <sys/types.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <note.h>
#include <net/if.h>
#include <net/route.h>
#include <sys/sockio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <pthread.h>
#include <syslog.h>
#include <ucred.h>
#include <priv.h>

#include <smbsrv/libsmb.h>
#include <netsmb/spnego.h>

#include "smbd.h"
#include "smbd_authsvc.h"

/* Arbitrary value outside the (small) range of valid OIDs */
#define special_mech_raw_NTLMSSP        (spnego_mech_oid_NTLMSSP + 100)

static struct sockaddr_un smbauth_sockname = {
        AF_UNIX, SMB_AUTHSVC_SOCKNAME };

typedef struct spnego_mech_handler {
        int mh_oid; /* SPNEGO_MECH_OID */
        int (*mh_init)(authsvc_context_t *);
        int (*mh_work)(authsvc_context_t *);
        void (*mh_fini)(authsvc_context_t *);
} spnego_mech_handler_t;

static int smbd_authsock_create(void);
static void smbd_authsock_destroy(void);
static void *smbd_authsvc_listen(void *);
static void *smbd_authsvc_work(void *);
static void smbd_authsvc_flood(void);

static int smbd_authsvc_oldreq(authsvc_context_t *);
static int smbd_authsvc_clinfo(authsvc_context_t *);
static int smbd_authsvc_esfirst(authsvc_context_t *);
static int smbd_authsvc_esnext(authsvc_context_t *);
static int smbd_authsvc_escmn(authsvc_context_t *);
static int smbd_authsvc_newmech(authsvc_context_t *);
static int smbd_authsvc_gettoken(authsvc_context_t *);
static int smbd_raw_ntlmssp_esfirst(authsvc_context_t *);
static int smbd_raw_ntlmssp_esnext(authsvc_context_t *);

/*
 * We can get relatively large tokens now, thanks to krb5 PAC.
 * Might be better to size these buffers dynamically, but these
 * are all short-lived so not bothering with that for now.
 */
int smbd_authsvc_bufsize = 65000;

static mutex_t smbd_authsvc_mutex = DEFAULTMUTEX;

/*
 * The maximum number of authentication thread is limited by the
 * smbsrv smb_threshold_...(->sv_ssetup_ct) mechanism.  However,
 * due to occasional delays closing these auth. sockets, we need
 * a little "slack" on the number of threads we'll allow, as
 * compared with the in-kernel limit.  We could perhaps just
 * remove this limit now, but want it for extra safety.
 */
int smbd_authsvc_maxthread = SMB_AUTHSVC_MAXTHREAD + 32;
int smbd_authsvc_thrcnt = 0;    /* current thrcnt */
int smbd_authsvc_hiwat = 0;     /* largest thrcnt seen */
#ifdef DEBUG
int smbd_authsvc_slowdown = 0;
#endif

/*
 * These are the mechanisms we support, in order of preference.
 * But note: it's really the _client's_ preference that matters.
 * See &pref in the spnegoIsMechTypeAvailable() calls below.
 * Careful with this table; the code below knows its format and
 * may skip the fist two entries to omit Kerberos.
 */
static const spnego_mech_handler_t
mech_table[] = {
        {
                spnego_mech_oid_Kerberos_V5,
                smbd_krb5ssp_init,
                smbd_krb5ssp_work,
                smbd_krb5ssp_fini
        },
        {
                spnego_mech_oid_Kerberos_V5_Legacy,
                smbd_krb5ssp_init,
                smbd_krb5ssp_work,
                smbd_krb5ssp_fini
        },
#define MECH_TBL_IDX_NTLMSSP    2
        {
                spnego_mech_oid_NTLMSSP,
                smbd_ntlmssp_init,
                smbd_ntlmssp_work,
                smbd_ntlmssp_fini
        },
        {
                /* end marker */
                spnego_mech_oid_NotUsed,
                NULL, NULL, NULL
        },
};

static const spnego_mech_handler_t
smbd_auth_mech_raw_ntlmssp = {
        special_mech_raw_NTLMSSP,
        smbd_ntlmssp_init,
        smbd_ntlmssp_work,
        smbd_ntlmssp_fini
};


/*
 * Start the authentication service.
 * Returns non-zero on error.
 */
int
smbd_authsvc_start(void)
{
        pthread_attr_t  attr;
        pthread_t       tid;
        int             rc;

        rc = smbd_authsock_create();
        if (rc)
                return (rc);

        (void) pthread_attr_init(&attr);
        (void) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        rc = pthread_create(&tid, &attr, smbd_authsvc_listen, &smbd);
        (void) pthread_attr_destroy(&attr);
        if (rc) {
                smbd_authsock_destroy();
                return (rc);
        }

        smbd.s_authsvc_tid = tid;
        return (0);
}

void
smbd_authsvc_stop(void)
{

        if (smbd.s_authsvc_tid != 0) {
                (void) pthread_kill(smbd.s_authsvc_tid, SIGTERM);
                smbd.s_authsvc_tid = 0;
        }
}

static int
smbd_authsock_create(void)
{
        int sock = -1;

        sock = socket(AF_UNIX, SOCK_STREAM, 0);
        if (sock < 0) {
                smbd_report("authsvc, socket create failed, %d", errno);
                return (errno);
        }

        (void) unlink(smbauth_sockname.sun_path);
        if (bind(sock, (struct sockaddr *)&smbauth_sockname,
            sizeof (smbauth_sockname)) < 0) {
                smbd_report("authsvc, socket bind failed, %d", errno);
                (void) close(sock);
                return (errno);
        }

        if (listen(sock, SOMAXCONN) < 0) {
                smbd_report("authsvc, socket listen failed, %d", errno);
                (void) close(sock);
                return (errno);
        }

        smbd.s_authsvc_sock = sock;
        return (0);
}

static void
smbd_authsock_destroy(void)
{
        int fid;

        if ((fid = smbd.s_authsvc_sock) != -1) {
                smbd.s_authsvc_sock = -1;
                (void) close(fid);
        }
}

#ifndef FKSMBD
/*
 * Decide whether to communicate with the client on this AF_UNIX socket.
 * Normally the caller should be the (in-kernel) SMB service which has
 * (typically) all privileges.  We test for PRIV_SYS_SMB here, which
 * only the SMB service should have.
 */
static boolean_t
authsock_has_priv(int sock)
{
        ucred_t *uc = NULL;
        const priv_set_t *ps = NULL;
        boolean_t ret = B_FALSE;
        pid_t  clpid;

        if (getpeerucred(sock, &uc) != 0) {
                smbd_report("authsvc: getpeerucred err %d", errno);
                return (B_FALSE);
        }
        clpid = ucred_getpid(uc);
        ps = ucred_getprivset(uc, PRIV_EFFECTIVE);
        if (ps == NULL) {
                smbd_report("authsvc: ucred_getprivset failed");
                goto out;
        }

        /*
         * Require sys_smb priv.
         */
        if (priv_ismember(ps, PRIV_SYS_SMB)) {
                ret = B_TRUE;
                goto out;
        }

        if (smbd.s_debug) {
                smbd_report("authsvc: non-privileged client "
                    "PID = %d UID = %d",
                    (int)clpid, ucred_getruid(uc));
        }

out:
        /* ps is free'd with the ucred */
        if (uc != NULL)
                ucred_free(uc);

        return (ret);
}
#endif


static void *
smbd_authsvc_listen(void *arg)
{
        authsvc_context_t *ctx;
        pthread_attr_t  attr;
        pthread_t       tid;
        socklen_t       slen;
        int             ls, ns, rc;

        _NOTE(ARGUNUSED(arg))

        (void) pthread_attr_init(&attr);
        (void) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

        ls = smbd.s_authsvc_sock;
        for (;;) {

                slen = 0;
                ns = accept(ls, NULL, &slen);
                if (ns < 0) {
                        switch (errno) {
                        case ECONNABORTED:
                                continue;
                        case EINTR:
                                /* normal termination */
                                goto out;
                        default:
                                smbd_report("authsvc, socket accept failed,"
                                    " %d", errno);
                                goto out;
                        }
                }

#ifndef FKSMBD
                if (!authsock_has_priv(ns)) {
                        close(ns);
                        continue;
                }
#endif

                /*
                 * Limit the number of auth. sockets
                 * (and the threads that service them).
                 */
                (void) mutex_lock(&smbd_authsvc_mutex);
                if (smbd_authsvc_thrcnt >= smbd_authsvc_maxthread) {
                        (void) mutex_unlock(&smbd_authsvc_mutex);
                        (void) close(ns);
                        smbd_authsvc_flood();
                        continue;
                }
                smbd_authsvc_thrcnt++;
                if (smbd_authsvc_hiwat < smbd_authsvc_thrcnt)
                        smbd_authsvc_hiwat = smbd_authsvc_thrcnt;
                (void) mutex_unlock(&smbd_authsvc_mutex);

                ctx = smbd_authctx_create();
                if (ctx == NULL) {
                        smbd_report("authsvc, can't allocate context");
                        (void) mutex_lock(&smbd_authsvc_mutex);
                        smbd_authsvc_thrcnt--;
                        (void) mutex_unlock(&smbd_authsvc_mutex);
                        (void) close(ns);
                        smbd_nomem();
                }
                ctx->ctx_socket = ns;

                rc = pthread_create(&tid, &attr, smbd_authsvc_work, ctx);
                if (rc) {
                        smbd_report("authsvc, thread create failed, %d", rc);
                        (void) mutex_lock(&smbd_authsvc_mutex);
                        smbd_authsvc_thrcnt--;
                        (void) mutex_unlock(&smbd_authsvc_mutex);
                        smbd_authctx_destroy(ctx);
                        ctx = NULL;
                        smbd_nomem();
                }
                ctx = NULL; /* given to the new thread or destroyed */
                (void) pthread_detach(tid);
        }

out:
        (void) pthread_attr_destroy(&attr);
        smbd_authsock_destroy();
        return (NULL);
}

static void
smbd_authsvc_flood(void)
{
        static uint_t count;
        static time_t last_report;
        time_t now = time(NULL);

        count++;
        if (last_report + 60 < now) {
                last_report = now;
                smbd_report("authsvc: flooded %u", count);
                count = 0;
        }
}

authsvc_context_t *
smbd_authctx_create(void)
{
        authsvc_context_t *ctx;

        ctx = malloc(sizeof (*ctx));
        if (ctx == NULL)
                return (NULL);
        bzero(ctx, sizeof (*ctx));

        ctx->ctx_irawlen = smbd_authsvc_bufsize;
        ctx->ctx_irawbuf = malloc(ctx->ctx_irawlen);
        ctx->ctx_orawlen = smbd_authsvc_bufsize;
        ctx->ctx_orawbuf = malloc(ctx->ctx_orawlen);
        if (ctx->ctx_irawbuf == NULL || ctx->ctx_orawbuf == NULL)
                goto errout;

        ctx->ctx_ibodylen = smbd_authsvc_bufsize;
        ctx->ctx_ibodybuf = malloc(ctx->ctx_ibodylen);
        ctx->ctx_obodylen = smbd_authsvc_bufsize;
        ctx->ctx_obodybuf = malloc(ctx->ctx_obodylen);
        if (ctx->ctx_ibodybuf == NULL || ctx->ctx_obodybuf == NULL)
                goto errout;

        return (ctx);

errout:
        smbd_authctx_destroy(ctx);
        return (NULL);
}

void
smbd_authctx_destroy(authsvc_context_t *ctx)
{
        if (ctx->ctx_socket != -1) {
                (void) close(ctx->ctx_socket);
                ctx->ctx_socket = -1;
        }

        if (ctx->ctx_token != NULL)
                smb_token_destroy(ctx->ctx_token);

        if (ctx->ctx_itoken != NULL)
                spnegoFreeData(ctx->ctx_itoken);
        if (ctx->ctx_otoken != NULL)
                spnegoFreeData(ctx->ctx_otoken);

        free(ctx->ctx_irawbuf);
        free(ctx->ctx_orawbuf);
        free(ctx->ctx_ibodybuf);
        free(ctx->ctx_obodybuf);

        free(ctx);
}

/*
 * Limit how long smbd_authsvc_work will wait for the client to
 * send us the next part of the authentication sequence.
 */
static struct timeval recv_tmo = { 30, 0 };

/*
 * Also set a timeout for send, where we're sending a response to
 * the client side (in smbsrv).  That should always be waiting in
 * recv by the time we send, so a short timeout is OK.
 */
static struct timeval send_tmo = { 15, 0 };

static void *
smbd_authsvc_work(void *arg)
{
        authsvc_context_t *ctx = arg;
        smb_lsa_msg_hdr_t       hdr;
        int sock = ctx->ctx_socket;
        int len, rc;

        if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO,
            (char *)&send_tmo,  sizeof (send_tmo)) != 0) {
                smbd_report("authsvc_work: set set timeout: %m");
                goto out;
        }

        if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
            (char *)&recv_tmo,  sizeof (recv_tmo)) != 0) {
                smbd_report("authsvc_work: set recv timeout: %m");
                goto out;
        }

        for (;;) {

                len = recv(sock, &hdr, sizeof (hdr), MSG_WAITALL);
                if (len <= 0) {
                        /* normal termination */
                        break;
                }
                if (len != sizeof (hdr)) {
                        smbd_report("authsvc_work: read header failed");
                        break;
                }

                if (hdr.lmh_msglen > smbd_authsvc_bufsize) {
                        smbd_report("authsvc_work: msg too large");
                        break;
                }

                if (hdr.lmh_msglen > 0) {
                        len = recv(sock, ctx->ctx_irawbuf, hdr.lmh_msglen,
                            MSG_WAITALL);
                        if (len != hdr.lmh_msglen) {
                                smbd_report("authsvc_work: read mesg failed");
                                break;
                        }
                }
                ctx->ctx_irawtype = hdr.lmh_msgtype;
                ctx->ctx_irawlen = hdr.lmh_msglen;
                ctx->ctx_orawlen = smbd_authsvc_bufsize;
                ctx->ctx_ibodylen = smbd_authsvc_bufsize;
                ctx->ctx_obodylen = smbd_authsvc_bufsize;

                /*
                 * The real work happens here.
                 */
                rc = smbd_authsvc_dispatch(ctx);
                if (rc)
                        break;

                hdr.lmh_msgtype = ctx->ctx_orawtype;
                hdr.lmh_msglen = ctx->ctx_orawlen;
                len = send(sock, &hdr, sizeof (hdr), 0);
                if (len != sizeof (hdr)) {
                        smbd_report("authsvc_work: send failed");
                        break;
                }

                if (ctx->ctx_orawlen > 0) {
                        len = send(sock, ctx->ctx_orawbuf,
                            ctx->ctx_orawlen, 0);
                        if (len != ctx->ctx_orawlen) {
                                smbd_report("authsvc_work: send failed");
                                break;
                        }
                }
        }

out:
        if (ctx->ctx_mh_fini)
                (ctx->ctx_mh_fini)(ctx);

        smbd_authctx_destroy(ctx);

        (void) mutex_lock(&smbd_authsvc_mutex);
        smbd_authsvc_thrcnt--;
        (void) mutex_unlock(&smbd_authsvc_mutex);

        return (NULL);  /* implied pthread_exit() */
}

/*
 * Dispatch based on message type LSA_MTYPE_...
 * Non-zero return here ends the conversation.
 */
int
smbd_authsvc_dispatch(authsvc_context_t *ctx)
{
        int rc;

        switch (ctx->ctx_irawtype) {

        case LSA_MTYPE_OLDREQ:
#ifdef DEBUG
                if (smbd_authsvc_slowdown)
                        (void) sleep(smbd_authsvc_slowdown);
#endif
                rc = smbd_authsvc_oldreq(ctx);
                break;

        case LSA_MTYPE_CLINFO:
                rc = smbd_authsvc_clinfo(ctx);
                break;

        case LSA_MTYPE_ESFIRST:
                rc = smbd_authsvc_esfirst(ctx);
                break;

        case LSA_MTYPE_ESNEXT:
#ifdef DEBUG
                if (smbd_authsvc_slowdown)
                        (void) sleep(smbd_authsvc_slowdown);
#endif
                rc = smbd_authsvc_esnext(ctx);
                break;

        case LSA_MTYPE_GETTOK:
                rc = smbd_authsvc_gettoken(ctx);
                break;

                /* response types */
        case LSA_MTYPE_OK:
        case LSA_MTYPE_ERROR:
        case LSA_MTYPE_TOKEN:
        case LSA_MTYPE_ES_CONT:
        case LSA_MTYPE_ES_DONE:
        default:
                return (-1);
        }

        if (rc == NT_STATUS_NO_MEMORY)
                smbd_nomem();

        if (rc != 0) {
                smb_lsa_eresp_t *er = ctx->ctx_orawbuf;
                ctx->ctx_orawtype = LSA_MTYPE_ERROR;
                ctx->ctx_orawlen = sizeof (*er);
                er->ler_ntstatus = rc;
                er->ler_errclass = 0;
                er->ler_errcode = 0;
        }
        return (0);
}

static int
smbd_authsvc_oldreq(authsvc_context_t *ctx)
{
        smb_logon_t     user_info;
        XDR             xdrs;
        smb_token_t     *token = NULL;
        int             rc = 0;

        bzero(&user_info, sizeof (user_info));
        xdrmem_create(&xdrs, ctx->ctx_irawbuf, ctx->ctx_irawlen,
            XDR_DECODE);
        if (!smb_logon_xdr(&xdrs, &user_info)) {
                xdr_destroy(&xdrs);
                return (NT_STATUS_INVALID_PARAMETER);
        }
        xdr_destroy(&xdrs);

        token = smbd_user_auth_logon(&user_info);
        xdr_free(smb_logon_xdr, (char *)&user_info);
        if (token == NULL) {
                rc = user_info.lg_status;
                if (rc == 0) /* should not happen */
                        rc = NT_STATUS_INTERNAL_ERROR;
                return (rc);
        }

        ctx->ctx_token = token;

        return (rc);
}

static int
smbd_authsvc_clinfo(authsvc_context_t *ctx)
{

        if (ctx->ctx_irawlen != sizeof (smb_lsa_clinfo_t))
                return (NT_STATUS_INTERNAL_ERROR);
        (void) memcpy(&ctx->ctx_clinfo, ctx->ctx_irawbuf,
            sizeof (smb_lsa_clinfo_t));

        ctx->ctx_orawtype = LSA_MTYPE_OK;
        ctx->ctx_orawlen = 0;
        return (0);
}

/*
 * Handle a security blob we've received from the client.
 * Incoming type: LSA_MTYPE_ESFIRST
 * Outgoing types: LSA_MTYPE_ES_CONT, LSA_MTYPE_ES_DONE,
 *   LSA_MTYPE_ERROR
 */
static int
smbd_authsvc_esfirst(authsvc_context_t *ctx)
{
        const spnego_mech_handler_t *mh;
        int idx, pref, rc;
        int best_pref = 1000;
        int best_mhidx = -1;

        /*
         * NTLMSSP header is 8+, SPNEGO is 10+
         */
        if (ctx->ctx_irawlen < 8) {
                smbd_report("authsvc: short blob");
                return (NT_STATUS_INVALID_PARAMETER);
        }

        /*
         * We could have "Raw NTLMSSP" here intead of SPNEGO.
         */
        if (bcmp(ctx->ctx_irawbuf, "NTLMSSP", 8) == 0) {
                rc = smbd_raw_ntlmssp_esfirst(ctx);
                return (rc);
        }

        /*
         * Parse the SPNEGO token, check its type.
         */
        rc = spnegoInitFromBinary(ctx->ctx_irawbuf,
            ctx->ctx_irawlen, &ctx->ctx_itoken);
        if (rc != 0) {
                smbd_report("authsvc: spnego parse failed");
                return (NT_STATUS_INVALID_PARAMETER);
        }

        rc = spnegoGetTokenType(ctx->ctx_itoken, &ctx->ctx_itoktype);
        if (rc != 0) {
                smbd_report("authsvc: spnego get token type failed");
                return (NT_STATUS_INVALID_PARAMETER);
        }

        if (ctx->ctx_itoktype != SPNEGO_TOKEN_INIT) {
                smbd_report("authsvc: spnego wrong token type %d",
                    ctx->ctx_itoktype);
                return (NT_STATUS_INVALID_PARAMETER);
        }

        /*
         * Figure out which mech type to use.  We want to use the
         * first of the client's supported mechanisms that we also
         * support.  Unfortunately, the spnego code does not have an
         * interface to walk the token's mech list, so we have to
         * ask about each mech type we know and keep track of which
         * was earliest in the token's mech list.
         *
         * Also, skip the Kerberos mechanisms in workgroup mode.
         */
        idx = 0;
        mh = mech_table;
        if (smb_config_get_secmode() != SMB_SECMODE_DOMAIN) {
                idx = MECH_TBL_IDX_NTLMSSP;
                mh = &mech_table[idx];
        }
        for (; mh->mh_init != NULL; idx++, mh++) {

                if (spnegoIsMechTypeAvailable(ctx->ctx_itoken,
                    mh->mh_oid, &pref) != 0)
                        continue;

                if (pref < best_pref) {
                        best_pref = pref;
                        best_mhidx = idx;
                }
        }
        if (best_mhidx == -1) {
                smbd_report("authsvc: no supported spnego mechanism");
                return (NT_STATUS_INVALID_PARAMETER);
        }

        /* Found a mutually agreeable mech. */
        mh = &mech_table[best_mhidx];
        ctx->ctx_mech_oid = mh->mh_oid;
        ctx->ctx_mh_work = mh->mh_work;
        ctx->ctx_mh_fini = mh->mh_fini;
        rc = mh->mh_init(ctx);
        if (rc != 0) {
                smbd_report("authsvc: mech init failed");
                return (rc);
        }

        /*
         * If the best supported mech was not the first in the list,
         * we need to ask the client to use a different one, and
         * skip (ignore) the provided token body.
         */
        if (best_pref != 0) {
                rc = smbd_authsvc_newmech(ctx);
        } else {
                /*
                 * Common to LSA_MTYPE_ESFIRST, LSA_MTYPE_ESNEXT
                 */
                rc = smbd_authsvc_escmn(ctx);
        }
        return (rc);
}

/*
 * Handle a security blob we've received from the client.
 * Incoming type: LSA_MTYPE_ESNEXT
 * Outgoing types: LSA_MTYPE_ES_CONT, LSA_MTYPE_ES_DONE,
 *   LSA_MTYPE_ERROR
 */
static int
smbd_authsvc_esnext(authsvc_context_t *ctx)
{
        int rc;

        /*
         * Make sure LSA_MTYPE_ESFIRST was handled
         * previously, so we have a work function.
         */
        if (ctx->ctx_mh_work == NULL)
                return (NT_STATUS_INVALID_PARAMETER);

        if (ctx->ctx_mech_oid == special_mech_raw_NTLMSSP) {
                rc = smbd_raw_ntlmssp_esnext(ctx);
                return (rc);
        }

        /*
         * Cleanup state from previous calls.
         */
        if (ctx->ctx_itoken != NULL) {
                spnegoFreeData(ctx->ctx_itoken);
                ctx->ctx_itoken = NULL;
        }

        /*
         * Parse the SPNEGO token, check its type.
         */
        rc = spnegoInitFromBinary(ctx->ctx_irawbuf,
            ctx->ctx_irawlen, &ctx->ctx_itoken);
        if (rc != 0)
                return (NT_STATUS_INVALID_PARAMETER);

        rc = spnegoGetTokenType(ctx->ctx_itoken, &ctx->ctx_itoktype);
        if (rc != 0)
                return (NT_STATUS_INVALID_PARAMETER);

        if (ctx->ctx_itoktype != SPNEGO_TOKEN_TARG)
                return (NT_STATUS_INVALID_PARAMETER);

        rc = smbd_authsvc_escmn(ctx);
        return (rc);
}

static int
smbd_authsvc_escmn(authsvc_context_t *ctx)
{
        SPNEGO_MECH_OID oid;
        ulong_t toklen;
        int rc;

        /*
         * Cleanup state from previous calls.
         */
        if (ctx->ctx_otoken != NULL) {
                spnegoFreeData(ctx->ctx_otoken);
                ctx->ctx_otoken = NULL;
        }

        /*
         * Extract the payload (mech token).
         */
        toklen = ctx->ctx_ibodylen;
        rc = spnegoGetMechToken(ctx->ctx_itoken,
            ctx->ctx_ibodybuf, &toklen);
        switch (rc) {
        case SPNEGO_E_SUCCESS:
                break;
        case SPNEGO_E_ELEMENT_UNAVAILABLE:
                toklen = 0;
                break;
        case SPNEGO_E_BUFFER_TOO_SMALL:
                return (NT_STATUS_BUFFER_TOO_SMALL);
        default:
                return (NT_STATUS_INTERNAL_ERROR);
        }
        ctx->ctx_ibodylen = toklen;

        /*
         * Now that we have the incoming "body" (mech. token),
         * call the back-end mech-specific work function to
         * create the outgoing "body" (mech. token).
         *
         * The worker must fill in:  ctx->ctx_negresult,
         * and: ctx->ctx_obodylen, but ctx->ctx_obodybuf
         * is optional, and is typically NULL after the
         * final message of an auth sequence, where
         * negresult == spnego_negresult_complete.
         */
        rc = ctx->ctx_mh_work(ctx);
        if (rc != 0)
                return (rc);

        /*
         * Wrap the outgoing body in a negTokenTarg SPNEGO token.
         * The selected mech. OID is returned only when the
         * incoming token was of type SPNEGO_TOKEN_INIT.
         */
        if (ctx->ctx_itoktype == SPNEGO_TOKEN_INIT) {
                /* tell the client the selected mech. */
                oid = ctx->ctx_mech_oid;
        } else {
                /* Omit the "supported mech." field. */
                oid = spnego_mech_oid_NotUsed;
        }

        /*
         * Determine the spnego "negresult" from the
         * reply message type (from the work func).
         */
        switch (ctx->ctx_orawtype) {
        case LSA_MTYPE_ERROR:
                ctx->ctx_negresult = spnego_negresult_rejected;
                break;
        case LSA_MTYPE_ES_DONE:
                ctx->ctx_negresult = spnego_negresult_success;
                break;
        case LSA_MTYPE_ES_CONT:
                ctx->ctx_negresult = spnego_negresult_incomplete;
                break;
        default:
                return (-1);
        }

        rc = spnegoCreateNegTokenTarg(
            oid,
            ctx->ctx_negresult,
            ctx->ctx_obodybuf, /* may be NULL */
            ctx->ctx_obodylen,
            NULL, 0,
            &ctx->ctx_otoken);
        if (rc != 0)
                return (NT_STATUS_INTERNAL_ERROR);

        /*
         * Convert the SPNEGO token into binary form,
         * writing it to the output buffer.
         */
        toklen = smbd_authsvc_bufsize;
        rc = spnegoTokenGetBinary(ctx->ctx_otoken,
            (uchar_t *)ctx->ctx_orawbuf, &toklen);
        if (rc != 0)
                rc = NT_STATUS_INTERNAL_ERROR;
        ctx->ctx_orawlen = (uint_t)toklen;

        return (rc);
}

/*
 * The first NegTokenInit we receive, handled in smbd_authsvc_esfirst,
 * contains a list of supported mechanisms, in order from the client's
 * most preferred to least preferred. The token also contains a body
 * for the first mechanism listed, which is used immediately if the
 * first mechanism is mutually agreeable.  If the first mechanism is
 * not supported on our side, we must "propose a new mechanism" from
 * the list.  Our caller has selected a mech and initialized the ctx
 * mech functions.  Here compose a reply with an empty body and the
 * proposed new mechanism OID.  The token body received is for some
 * other mech, so we skip calling the work function with that token.
 *
 * Just send a NegTokenTarg with an empty body and the OID for the
 * proposed mechanism.  The next message should be the real first
 * token for this mechanism, handled in smbd_authsvc_exnext.
 */
static int
smbd_authsvc_newmech(authsvc_context_t *ctx)
{
        ulong_t toklen;
        int rc;

        /*
         * Don't call mh_work here.
         * Just tell the clint the selected mech.
         */
        ctx->ctx_ibodylen = 0;
        ctx->ctx_orawtype = LSA_MTYPE_ES_CONT;
        ctx->ctx_obodylen = 0;
        ctx->ctx_negresult = spnego_negresult_request_mic;

        rc = spnegoCreateNegTokenTarg(
            ctx->ctx_mech_oid,
            ctx->ctx_negresult,
            NULL, 0,
            NULL, 0,
            &ctx->ctx_otoken);
        if (rc != 0)
                return (NT_STATUS_INTERNAL_ERROR);

        /*
         * Convert the SPNEGO token into binary form,
         * writing it to the output buffer.
         */
        toklen = smbd_authsvc_bufsize;
        rc = spnegoTokenGetBinary(ctx->ctx_otoken,
            (uchar_t *)ctx->ctx_orawbuf, &toklen);
        if (rc)
                rc = NT_STATUS_INTERNAL_ERROR;
        ctx->ctx_orawlen = (uint_t)toklen;

        return (rc);
}

/*
 * Wrapper for "Raw NTLMSSP", which is exactly like the
 * normal (SPNEGO-wrapped) NTLMSSP but without SPNEGO.
 * Setup back-end handler for: special_mech_raw_NTLMSSP
 * Compare with smbd_authsvc_esfirst().
 */
static int
smbd_raw_ntlmssp_esfirst(authsvc_context_t *ctx)
{
        const spnego_mech_handler_t *mh;
        int rc;

        mh = &smbd_auth_mech_raw_ntlmssp;
        rc = mh->mh_init(ctx);
        if (rc != 0)
                return (rc);

        ctx->ctx_mech_oid = mh->mh_oid;
        ctx->ctx_mh_work = mh->mh_work;
        ctx->ctx_mh_fini = mh->mh_fini;

        rc = smbd_raw_ntlmssp_esnext(ctx);

        return (rc);
}


/*
 * Wrapper for "Raw NTLMSSP", which is exactly like the
 * normal (SPNEGO-wrapped) NTLMSSP but without SPNEGO.
 * Just copy "raw" to "body", and vice versa.
 * Compare with smbd_authsvc_esnext, smbd_authsvc_escmn
 */
static int
smbd_raw_ntlmssp_esnext(authsvc_context_t *ctx)
{
        int rc;

        ctx->ctx_ibodylen = ctx->ctx_irawlen;
        (void) memcpy(ctx->ctx_ibodybuf,
            ctx->ctx_irawbuf, ctx->ctx_irawlen);

        rc = ctx->ctx_mh_work(ctx);

        ctx->ctx_orawlen = ctx->ctx_obodylen;
        (void) memcpy(ctx->ctx_orawbuf,
            ctx->ctx_obodybuf, ctx->ctx_obodylen);

        return (rc);
}


/*
 * After a successful authentication, request the access token.
 */
static int
smbd_authsvc_gettoken(authsvc_context_t *ctx)
{
        XDR             xdrs;
        smb_token_t     *token = NULL;
        int             rc = 0;
        int             len;

        if ((token = ctx->ctx_token) == NULL)
                return (NT_STATUS_ACCESS_DENIED);

        /*
         * Encode the token response
         */
        len = xdr_sizeof(smb_token_xdr, token);
        if (len > ctx->ctx_orawlen) {
                if ((ctx->ctx_orawbuf = realloc(ctx->ctx_orawbuf, len)) ==
                    NULL) {
                        return (NT_STATUS_NO_MEMORY);
                }
        }

        ctx->ctx_orawtype = LSA_MTYPE_TOKEN;
        ctx->ctx_orawlen = len;
        xdrmem_create(&xdrs, ctx->ctx_orawbuf, len, XDR_ENCODE);
        if (!smb_token_xdr(&xdrs, token))
                rc = NT_STATUS_INTERNAL_ERROR;
        xdr_destroy(&xdrs);

        return (rc);
}

/*
 * Initialization time code to figure out what mechanisms we support.
 * Careful with this table; the code below knows its format and may
 * skip the fist two entries to omit Kerberos.
 */
static SPNEGO_MECH_OID MechTypeList[] = {
        spnego_mech_oid_Kerberos_V5,
        spnego_mech_oid_Kerberos_V5_Legacy,
#define MECH_OID_IDX_NTLMSSP    2
        spnego_mech_oid_NTLMSSP,
};
static int MechTypeCnt = sizeof (MechTypeList) /
        sizeof (MechTypeList[0]);

/* This string is just like Windows. */
static char IgnoreSPN[] = "not_defined_in_RFC4178@please_ignore";

/*
 * Build the SPNEGO "hint" token based on the
 * configured authentication mechanisms.
 * (NTLMSSP, and maybe Kerberos)
 */
void
smbd_get_authconf(smb_kmod_cfg_t *kcfg)
{
        SPNEGO_MECH_OID *mechList = MechTypeList;
        int mechCnt = MechTypeCnt;
        SPNEGO_TOKEN_HANDLE hSpnegoToken = NULL;
        uchar_t *pBuf = kcfg->skc_negtok;
        uint32_t *pBufLen = &kcfg->skc_negtok_len;
        ulong_t tLen = sizeof (kcfg->skc_negtok);
        int rc;

        /*
         * In workgroup mode, skip Kerberos.
         */
        if (smb_config_get_secmode() != SMB_SECMODE_DOMAIN) {
                mechList += MECH_OID_IDX_NTLMSSP;
                mechCnt  -= MECH_OID_IDX_NTLMSSP;
        }

        rc = spnegoCreateNegTokenHint(mechList, mechCnt,
            (uchar_t *)IgnoreSPN, &hSpnegoToken);
        if (rc != SPNEGO_E_SUCCESS) {
                syslog(LOG_DEBUG, "smb_config_get_negtok: "
                    "spnegoCreateNegTokenHint, rc=%d", rc);
                *pBufLen = 0;
                return;
        }
        rc = spnegoTokenGetBinary(hSpnegoToken, pBuf, &tLen);
        if (rc != SPNEGO_E_SUCCESS) {
                syslog(LOG_DEBUG, "smb_config_get_negtok: "
                    "spnegoTokenGetBinary, rc=%d", rc);
                *pBufLen = 0;
        } else {
                *pBufLen = (uint32_t)tLen;
        }
        spnegoFreeData(hSpnegoToken);
}