/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 * COPYRIGHT (C) 2006,2007
 * THE REGENTS OF THE UNIVERSITY OF MICHIGAN
 * ALL RIGHTS RESERVED
 *
 * Permission is granted to use, copy, create derivative works
 * and redistribute this software and such derivative works
 * for any purpose, so long as the name of The University of
 * Michigan is not used in any advertising or publicity
 * pertaining to the use of distribution of this software
 * without specific, written prior authorization.  If the
 * above copyright notice or any other identification of the
 * University of Michigan is included in any copy of any
 * portion of this software, then the disclaimer below must
 * also be included.
 *
 * THIS SOFTWARE IS PROVIDED AS IS, WITHOUT REPRESENTATION
 * FROM THE UNIVERSITY OF MICHIGAN AS TO ITS FITNESS FOR ANY
 * PURPOSE, AND WITHOUT WARRANTY BY THE UNIVERSITY OF
 * MICHIGAN OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
 * WITHOUT LIMITATION THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
 * REGENTS OF THE UNIVERSITY OF MICHIGAN SHALL NOT BE LIABLE
 * FOR ANY DAMAGES, INCLUDING SPECIAL, INDIRECT, INCIDENTAL, OR
 * CONSEQUENTIAL DAMAGES, WITH RESPECT TO ANY CLAIM ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OF THE SOFTWARE, EVEN
 * IF IT HAS BEEN OR IS HEREAFTER ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGES.
 */

#include <k5-int.h>
#include "pkinit.h"
#include "krb5/certauth_plugin.h"

/* Aliases used by the built-in certauth modules */
struct certauth_req_opts {
    krb5_kdcpreauth_callbacks cb;
    krb5_kdcpreauth_rock rock;
    pkinit_kdc_context plgctx;
    pkinit_kdc_req_context reqctx;
};

typedef struct certauth_module_handle_st {
    struct krb5_certauth_vtable_st vt;
    krb5_certauth_moddata moddata;
} *certauth_handle;

struct krb5_kdcpreauth_moddata_st {
    pkinit_kdc_context *realm_contexts;
    certauth_handle *certauth_modules;
};

static krb5_error_code
pkinit_init_kdc_req_context(krb5_context, pkinit_kdc_req_context *blob);

static void
pkinit_fini_kdc_req_context(krb5_context context, void *blob);

static void
pkinit_server_plugin_fini_realm(krb5_context context,
                                pkinit_kdc_context plgctx);

static void
pkinit_server_plugin_fini(krb5_context context,
                          krb5_kdcpreauth_moddata moddata);

static pkinit_kdc_context
pkinit_find_realm_context(krb5_context context,
                          krb5_kdcpreauth_moddata moddata,
                          krb5_principal princ);

static void
free_realm_contexts(krb5_context context, pkinit_kdc_context *realm_contexts)
{
    int i;

    if (realm_contexts == NULL)
        return;
    for (i = 0; realm_contexts[i] != NULL; i++)
        pkinit_server_plugin_fini_realm(context, realm_contexts[i]);
    pkiDebug("%s: freeing context at %p\n", __FUNCTION__, realm_contexts);
    free(realm_contexts);
}

static void
free_certauth_handles(krb5_context context, certauth_handle *list)
{
    int i;

    if (list == NULL)
        return;
    for (i = 0; list[i] != NULL; i++) {
        if (list[i]->vt.fini != NULL)
            list[i]->vt.fini(context, list[i]->moddata);
        free(list[i]);
    }
    free(list);
}

static krb5_error_code
pkinit_create_edata(krb5_context context,
                    pkinit_plg_crypto_context plg_cryptoctx,
                    pkinit_req_crypto_context req_cryptoctx,
                    pkinit_identity_crypto_context id_cryptoctx,
                    pkinit_plg_opts *opts,
                    krb5_error_code err_code,
                    krb5_pa_data ***e_data_out)
{
    krb5_error_code retval = KRB5KRB_ERR_GENERIC;

    pkiDebug("pkinit_create_edata: creating edata for error %d (%s)\n",
             err_code, error_message(err_code));
    switch(err_code) {
    case KRB5KDC_ERR_CANT_VERIFY_CERTIFICATE:
        retval = pkinit_create_td_trusted_certifiers(context,
                                                     plg_cryptoctx, req_cryptoctx, id_cryptoctx, e_data_out);
        break;
    case KRB5KDC_ERR_DH_KEY_PARAMETERS_NOT_ACCEPTED:
        retval = pkinit_create_td_dh_parameters(context, plg_cryptoctx,
                                                req_cryptoctx, id_cryptoctx, opts, e_data_out);
        break;
    case KRB5KDC_ERR_INVALID_CERTIFICATE:
    case KRB5KDC_ERR_REVOKED_CERTIFICATE:
        retval = pkinit_create_td_invalid_certificate(context,
                                                      plg_cryptoctx, req_cryptoctx, id_cryptoctx, e_data_out);
        break;
    default:
        pkiDebug("no edata needed for error %d (%s)\n",
                 err_code, error_message(err_code));
        retval = 0;
        goto cleanup;
    }

cleanup:

    return retval;
}

static void
pkinit_server_get_edata(krb5_context context,
                        krb5_kdc_req *request,
                        krb5_kdcpreauth_callbacks cb,
                        krb5_kdcpreauth_rock rock,
                        krb5_kdcpreauth_moddata moddata,
                        krb5_preauthtype pa_type,
                        krb5_kdcpreauth_edata_respond_fn respond,
                        void *arg)
{
    krb5_error_code retval = 0;
    pkinit_kdc_context plgctx = NULL;

    pkiDebug("pkinit_server_get_edata: entered!\n");


    /*
     * If we don't have a realm context for the given realm,
     * don't tell the client that we support pkinit!
     */
    plgctx = pkinit_find_realm_context(context, moddata, request->server);
    if (plgctx == NULL)
        retval = EINVAL;

    /* Send a freshness token if the client requested one. */
    if (!retval)
        cb->send_freshness_token(context, rock);

    (*respond)(arg, retval, NULL);
}

static krb5_error_code
verify_client_san(krb5_context context,
                  pkinit_kdc_context plgctx,
                  pkinit_kdc_req_context reqctx,
                  krb5_kdcpreauth_callbacks cb,
                  krb5_kdcpreauth_rock rock,
                  krb5_const_principal client,
                  int *valid_san)
{
    krb5_error_code retval;
    krb5_principal *princs = NULL, upn;
    krb5_boolean match;
    char **upns = NULL;
    int i;
#ifdef DEBUG_SAN_INFO
    char *client_string = NULL, *san_string;
#endif

    *valid_san = 0;
    retval = crypto_retrieve_cert_sans(context, plgctx->cryptoctx,
                                       reqctx->cryptoctx, plgctx->idctx,
                                       &princs,
                                       plgctx->opts->allow_upn ? &upns : NULL,
                                       NULL);
    if (retval) {
        pkiDebug("%s: error from retrieve_certificate_sans()\n", __FUNCTION__);
        retval = KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
        goto out;
    }

    if (princs == NULL && upns == NULL) {
        TRACE_PKINIT_SERVER_NO_SAN(context);
        retval = ENOENT;
        goto out;
    }

#ifdef DEBUG_SAN_INFO
    krb5_unparse_name(context, client, &client_string);
#endif
    pkiDebug("%s: Checking pkinit sans\n", __FUNCTION__);
    for (i = 0; princs != NULL && princs[i] != NULL; i++) {
#ifdef DEBUG_SAN_INFO
        krb5_unparse_name(context, princs[i], &san_string);
        pkiDebug("%s: Comparing client '%s' to pkinit san value '%s'\n",
                 __FUNCTION__, client_string, san_string);
        krb5_free_unparsed_name(context, san_string);
#endif
        if (cb->match_client(context, rock, princs[i])) {
            TRACE_PKINIT_SERVER_MATCHING_SAN_FOUND(context);
            *valid_san = 1;
            retval = 0;
            goto out;
        }
    }
    pkiDebug("%s: no pkinit san match found\n", __FUNCTION__);
    /*
     * XXX if cert has names but none match, should we
     * be returning KRB5KDC_ERR_CLIENT_NAME_MISMATCH here?
     */

    if (upns == NULL) {
        pkiDebug("%s: no upn sans (or we wouldn't accept them anyway)\n",
                 __FUNCTION__);
        retval = KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
        goto out;
    }

    pkiDebug("%s: Checking upn sans\n", __FUNCTION__);
    for (i = 0; upns[i] != NULL; i++) {
#ifdef DEBUG_SAN_INFO
        pkiDebug("%s: Comparing client '%s' to upn san value '%s'\n",
                 __FUNCTION__, client_string, upns[i]);
#endif
        retval = krb5_parse_name_flags(context, upns[i],
                                       KRB5_PRINCIPAL_PARSE_ENTERPRISE, &upn);
        if (retval) {
            TRACE_PKINIT_SERVER_UPN_PARSE_FAIL(context, upns[i], retval);
            continue;
        }
        match = cb->match_client(context, rock, upn);
        krb5_free_principal(context, upn);
        if (match) {
            TRACE_PKINIT_SERVER_MATCHING_UPN_FOUND(context);
            *valid_san = 1;
            retval = 0;
            goto out;
        }
    }
    pkiDebug("%s: no upn san match found\n", __FUNCTION__);

    retval = 0;
out:
    if (princs != NULL) {
        for (i = 0; princs[i] != NULL; i++)
            krb5_free_principal(context, princs[i]);
        free(princs);
    }
    if (upns != NULL) {
        for (i = 0; upns[i] != NULL; i++)
            free(upns[i]);
        free(upns);
    }
#ifdef DEBUG_SAN_INFO
    if (client_string != NULL)
        krb5_free_unparsed_name(context, client_string);
#endif
    pkiDebug("%s: returning retval %d, valid_san %d\n",
             __FUNCTION__, retval, *valid_san);
    return retval;
}

static krb5_error_code
verify_client_eku(krb5_context context,
                  pkinit_kdc_context plgctx,
                  pkinit_kdc_req_context reqctx,
                  int *eku_accepted)
{
    krb5_error_code retval;

    *eku_accepted = 0;

    if (plgctx->opts->require_eku == 0) {
        TRACE_PKINIT_SERVER_EKU_SKIP(context);
        *eku_accepted = 1;
        retval = 0;
        goto out;
    }

    retval = crypto_check_cert_eku(context, plgctx->cryptoctx,
                                   reqctx->cryptoctx, plgctx->idctx,
                                   0, /* kdc cert */
                                   plgctx->opts->accept_secondary_eku,
                                   eku_accepted);
    if (retval) {
        pkiDebug("%s: Error from crypto_check_cert_eku %d (%s)\n",
                 __FUNCTION__, retval, error_message(retval));
        goto out;
    }

out:
    pkiDebug("%s: returning retval %d, eku_accepted %d\n",
             __FUNCTION__, retval, *eku_accepted);
    return retval;
}


/* Run the received, verified certificate through certauth modules, to verify
 * that it is authorized to authenticate as client. */
static krb5_error_code
authorize_cert(krb5_context context, certauth_handle *certauth_modules,
               pkinit_kdc_context plgctx, pkinit_kdc_req_context reqctx,
               krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,
               krb5_principal client, krb5_boolean *hwauth_out)
{
    krb5_error_code ret;
    certauth_handle h;
    struct certauth_req_opts opts;
    krb5_boolean accepted = FALSE, hwauth = FALSE;
    uint8_t *cert;
    size_t i, cert_len;
    void *db_ent = NULL;
    char **ais = NULL, **ai = NULL;

    /* Re-encode the received certificate into DER, which is extra work, but
     * avoids creating an X.509 library dependency in the interface. */
    ret = crypto_encode_der_cert(context, reqctx->cryptoctx, &cert, &cert_len);
    if (ret)
        goto cleanup;

    /* Set options for the builtin module. */
    opts.plgctx = plgctx;
    opts.reqctx = reqctx;
    opts.cb = cb;
    opts.rock = rock;

    db_ent = cb->client_entry(context, rock);

    /*
     * Check the certificate against each certauth module.  For the certificate
     * to be authorized at least one module must return 0 or
     * KRB5_CERTAUTH_HWAUTH, and no module can return an error code other than
     * KRB5_PLUGIN_NO_HANDLE (pass).  Add indicators from modules that return 0
     * or pass.
     */
    ret = KRB5_PLUGIN_NO_HANDLE;
    for (i = 0; certauth_modules != NULL && certauth_modules[i] != NULL; i++) {
        h = certauth_modules[i];
        TRACE_PKINIT_SERVER_CERT_AUTH(context, h->vt.name);
        ret = h->vt.authorize(context, h->moddata, cert, cert_len, client,
                              &opts, db_ent, &ais);
        if (ret == 0)
            accepted = TRUE;
        else if (ret == KRB5_CERTAUTH_HWAUTH)
            accepted = hwauth = TRUE;
        else if (ret != KRB5_PLUGIN_NO_HANDLE)
            goto cleanup;

        if (ais != NULL) {
            /* Assert authentication indicators from the module. */
            for (ai = ais; *ai != NULL; ai++) {
                ret = cb->add_auth_indicator(context, rock, *ai);
                if (ret)
                    goto cleanup;
            }
            h->vt.free_ind(context, h->moddata, ais);
            ais = NULL;
        }
    }

    *hwauth_out = hwauth;
    ret = accepted ? 0 : KRB5KDC_ERR_CLIENT_NAME_MISMATCH;

cleanup:
    free(cert);
    return ret;
}

/* Return an error if freshness tokens are required and one was not received.
 * Log an appropriate message indicating whether a valid token was received. */
static krb5_error_code
check_log_freshness(krb5_context context, pkinit_kdc_context plgctx,
                    krb5_kdc_req *request, krb5_boolean valid_freshness_token)
{
    krb5_error_code ret;
    char *name = NULL;

    ret = krb5_unparse_name(context, request->client, &name);
    if (ret)
        return ret;
    if (plgctx->opts->require_freshness && !valid_freshness_token) {
        com_err("", 0, _("PKINIT: no freshness token, rejecting auth from %s"),
                name);
        ret = KRB5KDC_ERR_PREAUTH_FAILED;
    } else if (valid_freshness_token) {
        com_err("", 0, _("PKINIT: freshness token received from %s"), name);
    } else {
        com_err("", 0, _("PKINIT: no freshness token received from %s"), name);
    }
    krb5_free_unparsed_name(context, name);
    return ret;
}

static void
pkinit_server_verify_padata(krb5_context context,
                            krb5_data *req_pkt,
                            krb5_kdc_req * request,
                            krb5_enc_tkt_part * enc_tkt_reply,
                            krb5_pa_data * data,
                            krb5_kdcpreauth_callbacks cb,
                            krb5_kdcpreauth_rock rock,
                            krb5_kdcpreauth_moddata moddata,
                            krb5_kdcpreauth_verify_respond_fn respond,
                            void *arg)
{
    krb5_error_code retval = 0;
    krb5_data authp_data = {0, 0, NULL}, krb5_authz = {0, 0, NULL};
    krb5_pa_pk_as_req *reqp = NULL;
    krb5_auth_pack *auth_pack = NULL;
    pkinit_kdc_context plgctx = NULL;
    pkinit_kdc_req_context reqctx = NULL;
    krb5_checksum cksum = {0, 0, 0, NULL};
    krb5_data *der_req = NULL;
    krb5_data k5data, *ftoken;
    int is_signed = 1;
    krb5_pa_data **e_data = NULL;
    krb5_kdcpreauth_modreq modreq = NULL;
    krb5_boolean valid_freshness_token = FALSE, hwauth = FALSE;
    char **sp;

    pkiDebug("pkinit_verify_padata: entered!\n");
    if (data == NULL || data->length <= 0 || data->contents == NULL) {
        (*respond)(arg, EINVAL, NULL, NULL, NULL);
        return;
    }


    if (moddata == NULL) {
        (*respond)(arg, EINVAL, NULL, NULL, NULL);
        return;
    }

    plgctx = pkinit_find_realm_context(context, moddata, request->server);
    if (plgctx == NULL) {
        (*respond)(arg, EINVAL, NULL, NULL, NULL);
        return;
    }

#ifdef DEBUG_ASN1
    print_buffer_bin(data->contents, data->length, "/tmp/kdc_as_req");
#endif
    /* create a per-request context */
    retval = pkinit_init_kdc_req_context(context, &reqctx);
    if (retval)
        goto cleanup;
    reqctx->pa_type = data->pa_type;

    PADATA_TO_KRB5DATA(data, &k5data);

    if (data->pa_type != KRB5_PADATA_PK_AS_REQ) {
        pkiDebug("unrecognized pa_type = %d\n", data->pa_type);
        retval = EINVAL;
        goto cleanup;
    }

    TRACE_PKINIT_SERVER_PADATA_VERIFY(context);
    retval = k5int_decode_krb5_pa_pk_as_req(&k5data, &reqp);
    if (retval) {
        pkiDebug("decode_krb5_pa_pk_as_req failed\n");
        goto cleanup;
    }
#ifdef DEBUG_ASN1
    print_buffer_bin(reqp->signedAuthPack.data, reqp->signedAuthPack.length,
                     "/tmp/kdc_signed_data");
#endif
    retval = cms_signeddata_verify(context, plgctx->cryptoctx,
                                   reqctx->cryptoctx, plgctx->idctx,
                                   CMS_SIGN_CLIENT,
                                   plgctx->opts->require_crl_checking,
                                   (unsigned char *)reqp->signedAuthPack.data,
                                   reqp->signedAuthPack.length,
                                   (unsigned char **)&authp_data.data,
                                   &authp_data.length,
                                   (unsigned char **)&krb5_authz.data,
                                   &krb5_authz.length, &is_signed);
    if (retval) {
        TRACE_PKINIT_SERVER_PADATA_VERIFY_FAIL(context);
        goto cleanup;
    }
    if (is_signed) {
        retval = authorize_cert(context, moddata->certauth_modules, plgctx,
                                reqctx, cb, rock, request->client, &hwauth);
        if (retval)
            goto cleanup;

    } else { /* !is_signed */
        if (!krb5_principal_compare(context, request->client,
                                    krb5_anonymous_principal())) {
            retval = KRB5KDC_ERR_PREAUTH_FAILED;
            krb5_set_error_message(context, retval,
                                   _("Pkinit request not signed, but client "
                                     "not anonymous."));
            goto cleanup;
        }
    }
#ifdef DEBUG_ASN1
    print_buffer_bin(authp_data.data, authp_data.length, "/tmp/kdc_auth_pack");
#endif

    OCTETDATA_TO_KRB5DATA(&authp_data, &k5data);
    retval = k5int_decode_krb5_auth_pack(&k5data, &auth_pack);
    if (retval) {
        pkiDebug("failed to decode krb5_auth_pack\n");
        goto cleanup;
    }

    retval = krb5_check_clockskew(context, auth_pack->pkAuthenticator.ctime);
    if (retval)
        goto cleanup;

    /* check dh parameters */
    if (auth_pack->clientPublicValue != NULL) {
        retval = server_check_dh(context, plgctx->cryptoctx,
                                 reqctx->cryptoctx, plgctx->idctx,
                                 &auth_pack->clientPublicValue->algorithm.parameters,
                                 plgctx->opts->dh_min_bits);
        if (retval) {
            pkiDebug("bad dh parameters\n");
            goto cleanup;
        }
    } else if (!is_signed) {
        /*Anonymous pkinit requires DH*/
        retval = KRB5KDC_ERR_PREAUTH_FAILED;
        krb5_set_error_message(context, retval,
                               _("Anonymous pkinit without DH public "
                                 "value not supported."));
        goto cleanup;
    }
    der_req = cb->request_body(context, rock);
    retval = krb5_c_make_checksum(context, CKSUMTYPE_NIST_SHA, NULL, 0,
                                  der_req, &cksum);
    if (retval) {
        pkiDebug("unable to calculate AS REQ checksum\n");
        goto cleanup;
    }
    if (cksum.length != auth_pack->pkAuthenticator.paChecksum.length ||
        k5_bcmp(cksum.contents, auth_pack->pkAuthenticator.paChecksum.contents,
                cksum.length) != 0) {
        pkiDebug("failed to match the checksum\n");
#ifdef DEBUG_CKSUM
        pkiDebug("calculating checksum on buf size (%d)\n", req_pkt->length);
        print_buffer(req_pkt->data, req_pkt->length);
        pkiDebug("received checksum type=%d size=%d ",
                 auth_pack->pkAuthenticator.paChecksum.checksum_type,
                 auth_pack->pkAuthenticator.paChecksum.length);
        print_buffer(auth_pack->pkAuthenticator.paChecksum.contents,
                     auth_pack->pkAuthenticator.paChecksum.length);
        pkiDebug("expected checksum type=%d size=%d ",
                 cksum.checksum_type, cksum.length);
        print_buffer(cksum.contents, cksum.length);
#endif

        retval = KRB5KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED;
        goto cleanup;
    }

    ftoken = auth_pack->pkAuthenticator.freshnessToken;
    if (ftoken != NULL) {
        retval = cb->check_freshness_token(context, rock, ftoken);
        if (retval)
            goto cleanup;
        valid_freshness_token = TRUE;
    }

    /* check if kdcPkId present and match KDC's subjectIdentifier */
    if (reqp->kdcPkId.data != NULL) {
        int valid_kdcPkId = 0;
        retval = pkinit_check_kdc_pkid(context, plgctx->cryptoctx,
                                       reqctx->cryptoctx, plgctx->idctx,
                                       (unsigned char *)reqp->kdcPkId.data,
                                       reqp->kdcPkId.length, &valid_kdcPkId);
        if (retval)
            goto cleanup;
        if (!valid_kdcPkId) {
            pkiDebug("kdcPkId in AS_REQ does not match KDC's cert; "
                     "RFC says to ignore and proceed\n");
        }
    }
    /* remember the decoded auth_pack for verify_padata routine */
    reqctx->rcv_auth_pack = auth_pack;
    auth_pack = NULL;

    if (is_signed) {
        retval = check_log_freshness(context, plgctx, request,
                                     valid_freshness_token);
        if (retval)
            goto cleanup;
    }

    if (is_signed && plgctx->auth_indicators != NULL) {
        /* Assert configured authentication indicators. */
        for (sp = plgctx->auth_indicators; *sp != NULL; sp++) {
            retval = cb->add_auth_indicator(context, rock, *sp);
            if (retval)
                goto cleanup;
        }
    }

    /* remember to set the PREAUTH flag in the reply */
    enc_tkt_reply->flags |= TKT_FLG_PRE_AUTH;
    if (hwauth)
        enc_tkt_reply->flags |= TKT_FLG_HW_AUTH;
    modreq = (krb5_kdcpreauth_modreq)reqctx;
    reqctx = NULL;

cleanup:
    if (retval && data->pa_type == KRB5_PADATA_PK_AS_REQ) {
        pkiDebug("pkinit_verify_padata failed: creating e-data\n");
        if (pkinit_create_edata(context, plgctx->cryptoctx, reqctx->cryptoctx,
                                plgctx->idctx, plgctx->opts, retval, &e_data))
            pkiDebug("pkinit_create_edata failed\n");
    }

    free_krb5_pa_pk_as_req(&reqp);
    free(cksum.contents);
    free(authp_data.data);
    free(krb5_authz.data);
    if (reqctx != NULL)
        pkinit_fini_kdc_req_context(context, reqctx);
    free_krb5_auth_pack(&auth_pack);

    (*respond)(arg, retval, modreq, e_data, NULL);
}
static krb5_error_code
return_pkinit_kx(krb5_context context, krb5_kdc_req *request,
                 krb5_kdc_rep *reply, krb5_keyblock *encrypting_key,
                 krb5_pa_data **out_padata)
{
    krb5_error_code ret = 0;
    krb5_keyblock *session = reply->ticket->enc_part2->session;
    krb5_keyblock *new_session = NULL;
    krb5_pa_data *pa = NULL;
    krb5_enc_data enc;
    krb5_data *scratch = NULL;

    *out_padata = NULL;
    enc.ciphertext.data = NULL;
    if (!krb5_principal_compare(context, request->client,
                                krb5_anonymous_principal()))
        return 0;
    /*
     * The KDC contribution key needs to be a fresh key of an enctype supported
     * by the client and server. The existing session key meets these
     * requirements so we use it.
     */
    ret = krb5_c_fx_cf2_simple(context, session, "PKINIT",
                               encrypting_key, "KEYEXCHANGE",
                               &new_session);
    if (ret)
        goto cleanup;
    ret = encode_krb5_encryption_key( session, &scratch);
    if (ret)
        goto cleanup;
    ret = krb5_encrypt_helper(context, encrypting_key,
                              KRB5_KEYUSAGE_PA_PKINIT_KX, scratch, &enc);
    if (ret)
        goto cleanup;
    memset(scratch->data, 0, scratch->length);
    krb5_free_data(context, scratch);
    scratch = NULL;
    ret = encode_krb5_enc_data(&enc, &scratch);
    if (ret)
        goto cleanup;
    pa = malloc(sizeof(krb5_pa_data));
    if (pa == NULL) {
        ret = ENOMEM;
        goto cleanup;
    }
    pa->pa_type = KRB5_PADATA_PKINIT_KX;
    pa->length = scratch->length;
    pa->contents = (krb5_octet *) scratch->data;
    *out_padata = pa;
    scratch->data = NULL;
    memset(session->contents, 0, session->length);
    krb5_free_keyblock_contents(context, session);
    *session = *new_session;
    new_session->contents = NULL;
cleanup:
    krb5_free_data_contents(context, &enc.ciphertext);
    krb5_free_keyblock(context, new_session);
    krb5_free_data(context, scratch);
    return ret;
}

static krb5_error_code
pkinit_pick_kdf_alg(krb5_context context, krb5_data **kdf_list,
                    krb5_data **alg_oid)
{
    krb5_error_code retval = 0;
    krb5_data *req_oid = NULL;
    const krb5_data *supp_oid = NULL;
    krb5_data *tmp_oid = NULL;
    int i, j = 0;

    /* if we don't find a match, return NULL value */
    *alg_oid = NULL;

    /* for each of the OIDs that the server supports... */
    for (i = 0; NULL != (supp_oid = supported_kdf_alg_ids[i]); i++) {
        /* if the requested OID is in the client's list, use it. */
        for (j = 0; NULL != (req_oid = kdf_list[j]); j++) {
            if ((req_oid->length == supp_oid->length) &&
                (0 == memcmp(req_oid->data, supp_oid->data, req_oid->length))) {
                tmp_oid = k5alloc(sizeof(krb5_data), &retval);
                if (retval)
                    goto cleanup;
                tmp_oid->data = k5memdup(supp_oid->data, supp_oid->length,
                                         &retval);
                if (retval)
                    goto cleanup;
                tmp_oid->length = supp_oid->length;
                *alg_oid = tmp_oid;
                /* don't free the OID in clean-up if we are returning it */
                tmp_oid = NULL;
                goto cleanup;
            }
        }
    }
cleanup:
    if (tmp_oid)
        krb5_free_data(context, tmp_oid);
    return retval;
}

static krb5_error_code
pkinit_server_return_padata(krb5_context context,
                            krb5_pa_data * padata,
                            krb5_data *req_pkt,
                            krb5_kdc_req * request,
                            krb5_kdc_rep * reply,
                            krb5_keyblock * encrypting_key,
                            krb5_pa_data ** send_pa,
                            krb5_kdcpreauth_callbacks cb,
                            krb5_kdcpreauth_rock rock,
                            krb5_kdcpreauth_moddata moddata,
                            krb5_kdcpreauth_modreq modreq)
{
    krb5_error_code retval = 0;
    krb5_data scratch = {0, 0, NULL};
    krb5_pa_pk_as_req *reqp = NULL;
    int i = 0;

    unsigned char *subjectPublicKey = NULL;
    unsigned char *dh_pubkey = NULL, *server_key = NULL;
    unsigned int subjectPublicKey_len = 0;
    unsigned int server_key_len = 0, dh_pubkey_len = 0;

    krb5_kdc_dh_key_info dhkey_info;
    krb5_data *encoded_dhkey_info = NULL;
    krb5_pa_pk_as_rep *rep = NULL;
    krb5_data *out_data = NULL;
    krb5_data secret;

    krb5_enctype enctype = -1;

    krb5_reply_key_pack *key_pack = NULL;
    krb5_data *encoded_key_pack = NULL;

    pkinit_kdc_context plgctx;
    pkinit_kdc_req_context reqctx;

    *send_pa = NULL;
    if (padata->pa_type == KRB5_PADATA_PKINIT_KX) {
        return return_pkinit_kx(context, request, reply,
                                encrypting_key, send_pa);
    }
    if (padata->length <= 0 || padata->contents == NULL)
        return 0;

    if (modreq == NULL) {
        pkiDebug("missing request context \n");
        return EINVAL;
    }

    plgctx = pkinit_find_realm_context(context, moddata, request->server);
    if (plgctx == NULL) {
        pkiDebug("Unable to locate correct realm context\n");
        return ENOENT;
    }

    TRACE_PKINIT_SERVER_RETURN_PADATA(context);
    reqctx = (pkinit_kdc_req_context)modreq;

    if (encrypting_key->contents) {
        free(encrypting_key->contents);
        encrypting_key->length = 0;
        encrypting_key->contents = NULL;
    }

    for(i = 0; i < request->nktypes; i++) {
        enctype = request->ktype[i];
        if (!krb5_c_valid_enctype(enctype))
            continue;
        else {
            pkiDebug("KDC picked etype = %d\n", enctype);
            break;
        }
    }

    if (i == request->nktypes) {
        retval = KRB5KDC_ERR_ETYPE_NOSUPP;
        goto cleanup;
    }

    init_krb5_pa_pk_as_rep(&rep);
    if (rep == NULL) {
        retval = ENOMEM;
        goto cleanup;
    }
    /* let's assume it's RSA. we'll reset it to DH if needed */
    rep->choice = choice_pa_pk_as_rep_encKeyPack;

    if (reqctx->rcv_auth_pack != NULL &&
        reqctx->rcv_auth_pack->clientPublicValue != NULL) {
        subjectPublicKey = (unsigned char *)
            reqctx->rcv_auth_pack->clientPublicValue->subjectPublicKey.data;
        subjectPublicKey_len =
            reqctx->rcv_auth_pack->clientPublicValue->subjectPublicKey.length;
        rep->choice = choice_pa_pk_as_rep_dhInfo;

        pkiDebug("received DH key delivery AS REQ\n");
        retval = server_process_dh(context, plgctx->cryptoctx,
                                   reqctx->cryptoctx, plgctx->idctx, subjectPublicKey,
                                   subjectPublicKey_len, &dh_pubkey, &dh_pubkey_len,
                                   &server_key, &server_key_len);
        if (retval) {
            pkiDebug("failed to process/create dh parameters\n");
            goto cleanup;
        }

        /*
         * This is DH, so don't generate the key until after we
         * encode the reply, because the encoded reply is needed
         * to generate the key in some cases.
         */

        dhkey_info.subjectPublicKey.length = dh_pubkey_len;
        dhkey_info.subjectPublicKey.data = (char *)dh_pubkey;
        dhkey_info.nonce = request->nonce;
        dhkey_info.dhKeyExpiration = 0;

        retval = k5int_encode_krb5_kdc_dh_key_info(&dhkey_info,
                                                   &encoded_dhkey_info);
        if (retval) {
            pkiDebug("encode_krb5_kdc_dh_key_info failed\n");
            goto cleanup;
        }
#ifdef DEBUG_ASN1
        print_buffer_bin((unsigned char *)encoded_dhkey_info->data,
                         encoded_dhkey_info->length,
                         "/tmp/kdc_dh_key_info");
#endif

        retval = cms_signeddata_create(context, plgctx->cryptoctx,
                                       reqctx->cryptoctx, plgctx->idctx,
                                       CMS_SIGN_SERVER, 1,
                                       (unsigned char *)
                                       encoded_dhkey_info->data,
                                       encoded_dhkey_info->length,
                                       (unsigned char **)
                                       &rep->u.dh_Info.dhSignedData.data,
                                       &rep->u.dh_Info.dhSignedData.length);
        if (retval) {
            pkiDebug("failed to create pkcs7 signed data\n");
            goto cleanup;
        }

    } else {
        pkiDebug("received RSA key delivery AS REQ\n");

        retval = krb5_c_make_random_key(context, enctype, encrypting_key);
        if (retval) {
            pkiDebug("unable to make a session key\n");
            goto cleanup;
        }

        init_krb5_reply_key_pack(&key_pack);
        if (key_pack == NULL) {
            retval = ENOMEM;
            goto cleanup;
        }

        retval = krb5_c_make_checksum(context, 0, encrypting_key,
                                      KRB5_KEYUSAGE_TGS_REQ_AUTH_CKSUM,
                                      req_pkt, &key_pack->asChecksum);
        if (retval) {
            pkiDebug("unable to calculate AS REQ checksum\n");
            goto cleanup;
        }
#ifdef DEBUG_CKSUM
        pkiDebug("calculating checksum on buf size = %d\n", req_pkt->length);
        print_buffer(req_pkt->data, req_pkt->length);
        pkiDebug("checksum size = %d\n", key_pack->asChecksum.length);
        print_buffer(key_pack->asChecksum.contents,
                     key_pack->asChecksum.length);
        pkiDebug("encrypting key (%d)\n", encrypting_key->length);
        print_buffer(encrypting_key->contents, encrypting_key->length);
#endif

        krb5_copy_keyblock_contents(context, encrypting_key,
                                    &key_pack->replyKey);

        retval = k5int_encode_krb5_reply_key_pack(key_pack,
                                                  &encoded_key_pack);
        if (retval) {
            pkiDebug("failed to encode reply_key_pack\n");
            goto cleanup;
        }

        rep->choice = choice_pa_pk_as_rep_encKeyPack;
        retval = cms_envelopeddata_create(context, plgctx->cryptoctx,
                                          reqctx->cryptoctx, plgctx->idctx,
                                          padata->pa_type, 1,
                                          (unsigned char *)
                                          encoded_key_pack->data,
                                          encoded_key_pack->length,
                                          (unsigned char **)
                                          &rep->u.encKeyPack.data,
                                          &rep->u.encKeyPack.length);
        if (retval) {
            pkiDebug("failed to create pkcs7 enveloped data: %s\n",
                     error_message(retval));
            goto cleanup;
        }
#ifdef DEBUG_ASN1
        print_buffer_bin((unsigned char *)encoded_key_pack->data,
                         encoded_key_pack->length,
                         "/tmp/kdc_key_pack");
        print_buffer_bin(rep->u.encKeyPack.data, rep->u.encKeyPack.length,
                         "/tmp/kdc_enc_key_pack");
#endif
    }

    if (rep->choice == choice_pa_pk_as_rep_dhInfo &&
        ((reqctx->rcv_auth_pack != NULL &&
          reqctx->rcv_auth_pack->supportedKDFs != NULL))) {

        /* If using the alg-agility KDF, put the algorithm in the reply
         * before encoding it.
         */
        if (reqctx->rcv_auth_pack != NULL &&
            reqctx->rcv_auth_pack->supportedKDFs != NULL) {
            retval = pkinit_pick_kdf_alg(context, reqctx->rcv_auth_pack->supportedKDFs,
                                         &(rep->u.dh_Info.kdfID));
            if (retval) {
                pkiDebug("pkinit_pick_kdf_alg failed: %s\n",
                         error_message(retval));
                goto cleanup;
            }
        }
    }

    retval = k5int_encode_krb5_pa_pk_as_rep(rep, &out_data);
    if (retval) {
        pkiDebug("failed to encode AS_REP\n");
        goto cleanup;
    }
#ifdef DEBUG_ASN1
    if (out_data != NULL)
        print_buffer_bin((unsigned char *)out_data->data, out_data->length,
                         "/tmp/kdc_as_rep");
#endif

    /* If this is DH, we haven't computed the key yet, so do it now. */
    if (rep->choice == choice_pa_pk_as_rep_dhInfo) {

        /* If mutually supported KDFs were found, use the algorithm agility
         * KDF. */
        if (rep->u.dh_Info.kdfID) {
            secret.data = (char *)server_key;
            secret.length = server_key_len;

            retval = pkinit_alg_agility_kdf(context, &secret,
                                            rep->u.dh_Info.kdfID,
                                            request->client, request->server,
                                            enctype, req_pkt, out_data,
                                            encrypting_key);
            if (retval) {
                pkiDebug("pkinit_alg_agility_kdf failed: %s\n",
                         error_message(retval));
                goto cleanup;
            }

            /* Otherwise, use the older octetstring2key() function */
        } else {
            retval = pkinit_octetstring2key(context, enctype, server_key,
                                            server_key_len, encrypting_key);
            if (retval) {
                pkiDebug("pkinit_octetstring2key failed: %s\n",
                         error_message(retval));
                goto cleanup;
            }
        }
    }

    *send_pa = malloc(sizeof(krb5_pa_data));
    if (*send_pa == NULL) {
        retval = ENOMEM;
        free(out_data->data);
        free(out_data);
        out_data = NULL;
        goto cleanup;
    }
    (*send_pa)->magic = KV5M_PA_DATA;
    (*send_pa)->pa_type = KRB5_PADATA_PK_AS_REP;
    (*send_pa)->length = out_data->length;
    (*send_pa)->contents = (krb5_octet *) out_data->data;

cleanup:
    pkinit_fini_kdc_req_context(context, reqctx);
    free(scratch.data);
    free(out_data);
    if (encoded_dhkey_info != NULL)
        krb5_free_data(context, encoded_dhkey_info);
    if (encoded_key_pack != NULL)
        krb5_free_data(context, encoded_key_pack);
    free(dh_pubkey);
    free(server_key);
    free_krb5_pa_pk_as_req(&reqp);
    free_krb5_pa_pk_as_rep(&rep);
    free_krb5_reply_key_pack(&key_pack);

    if (retval)
        pkiDebug("pkinit_verify_padata failure");

    return retval;
}

static int
pkinit_server_get_flags(krb5_context kcontext, krb5_preauthtype patype)
{
    if (patype == KRB5_PADATA_PKINIT_KX)
        return PA_INFO;
    /* PKINIT does not normally set the hw-authent ticket flag, but a
     * certauth module can cause it to do so. */
    return PA_SUFFICIENT | PA_REPLACES_KEY | PA_TYPED_E_DATA | PA_HARDWARE;
}

static krb5_preauthtype supported_server_pa_types[] = {
    KRB5_PADATA_PK_AS_REQ,
    KRB5_PADATA_PKINIT_KX,
    0
};

static void
pkinit_fini_kdc_profile(krb5_context context, pkinit_kdc_context plgctx)
{
    /*
     * There is nothing currently allocated by pkinit_init_kdc_profile()
     * which needs to be freed here.
     */
}

static krb5_error_code
pkinit_init_kdc_profile(krb5_context context, pkinit_kdc_context plgctx)
{
    krb5_error_code retval;
    char *eku_string = NULL, *ocsp_check = NULL;

    pkiDebug("%s: entered for realm %s\n", __FUNCTION__, plgctx->realmname);
    retval = pkinit_kdcdefault_string(context, plgctx->realmname,
                                      KRB5_CONF_PKINIT_IDENTITY,
                                      &plgctx->idopts->identity);
    if (retval != 0 || NULL == plgctx->idopts->identity) {
        retval = EINVAL;
        krb5_set_error_message(context, retval,
                               _("No pkinit_identity supplied for realm %s"),
                               plgctx->realmname);
        goto errout;
    }

    retval = pkinit_kdcdefault_strings(context, plgctx->realmname,
                                       KRB5_CONF_PKINIT_ANCHORS,
                                       &plgctx->idopts->anchors);
    if (retval != 0 || NULL == plgctx->idopts->anchors) {
        retval = EINVAL;
        krb5_set_error_message(context, retval,
                               _("No pkinit_anchors supplied for realm %s"),
                               plgctx->realmname);
        goto errout;
    }

    pkinit_kdcdefault_strings(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_POOL,
                              &plgctx->idopts->intermediates);

    pkinit_kdcdefault_strings(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_REVOKE,
                              &plgctx->idopts->crls);

    pkinit_kdcdefault_string(context, plgctx->realmname,
                             KRB5_CONF_PKINIT_KDC_OCSP,
                             &ocsp_check);
    if (ocsp_check != NULL) {
        free(ocsp_check);
        retval = ENOTSUP;
        krb5_set_error_message(context, retval,
                               _("OCSP is not supported: (realm: %s)"),
                               plgctx->realmname);
        goto errout;
    }

    pkinit_kdcdefault_integer(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_DH_MIN_BITS,
                              PKINIT_DEFAULT_DH_MIN_BITS,
                              &plgctx->opts->dh_min_bits);
    if (plgctx->opts->dh_min_bits < PKINIT_DH_MIN_CONFIG_BITS) {
        pkiDebug("%s: invalid value (%d < %d) for pkinit_dh_min_bits, "
                 "using default value (%d) instead\n", __FUNCTION__,
                 plgctx->opts->dh_min_bits, PKINIT_DH_MIN_CONFIG_BITS,
                 PKINIT_DEFAULT_DH_MIN_BITS);
        plgctx->opts->dh_min_bits = PKINIT_DEFAULT_DH_MIN_BITS;
    }

    pkinit_kdcdefault_boolean(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_ALLOW_UPN,
                              0, &plgctx->opts->allow_upn);

    pkinit_kdcdefault_boolean(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_REQUIRE_CRL_CHECKING,
                              0, &plgctx->opts->require_crl_checking);

    pkinit_kdcdefault_boolean(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_REQUIRE_FRESHNESS,
                              0, &plgctx->opts->require_freshness);

    pkinit_kdcdefault_string(context, plgctx->realmname,
                             KRB5_CONF_PKINIT_EKU_CHECKING,
                             &eku_string);
    if (eku_string != NULL) {
        if (strcasecmp(eku_string, "kpClientAuth") == 0) {
            plgctx->opts->require_eku = 1;
            plgctx->opts->accept_secondary_eku = 0;
        } else if (strcasecmp(eku_string, "scLogin") == 0) {
            plgctx->opts->require_eku = 1;
            plgctx->opts->accept_secondary_eku = 1;
        } else if (strcasecmp(eku_string, "none") == 0) {
            plgctx->opts->require_eku = 0;
            plgctx->opts->accept_secondary_eku = 0;
        } else {
            pkiDebug("%s: Invalid value for pkinit_eku_checking: '%s'\n",
                     __FUNCTION__, eku_string);
        }
        free(eku_string);
    }

    pkinit_kdcdefault_strings(context, plgctx->realmname,
                              KRB5_CONF_PKINIT_INDICATOR,
                              &plgctx->auth_indicators);

    return 0;
errout:
    pkinit_fini_kdc_profile(context, plgctx);
    return retval;
}

static pkinit_kdc_context
pkinit_find_realm_context(krb5_context context,
                          krb5_kdcpreauth_moddata moddata,
                          krb5_principal princ)
{
    int i;
    pkinit_kdc_context *realm_contexts;

    if (moddata == NULL)
        return NULL;

    realm_contexts = moddata->realm_contexts;
    if (realm_contexts == NULL)
        return NULL;

    for (i = 0; realm_contexts[i] != NULL; i++) {
        pkinit_kdc_context p = realm_contexts[i];

        if ((p->realmname_len == princ->realm.length) &&
            (strncmp(p->realmname, princ->realm.data, p->realmname_len) == 0)) {
            pkiDebug("%s: returning context at %p for realm '%s'\n",
                     __FUNCTION__, p, p->realmname);
            return p;
        }
    }
    pkiDebug("%s: unable to find realm context for realm '%.*s'\n",
             __FUNCTION__, princ->realm.length, princ->realm.data);
    return NULL;
}

static int
pkinit_server_plugin_init_realm(krb5_context context, const char *realmname,
                                pkinit_kdc_context *pplgctx)
{
    krb5_error_code retval = ENOMEM;
    pkinit_kdc_context plgctx = NULL;

    *pplgctx = NULL;

    plgctx = calloc(1, sizeof(*plgctx));
    if (plgctx == NULL)
        goto errout;

    pkiDebug("%s: initializing context at %p for realm '%s'\n",
             __FUNCTION__, plgctx, realmname);
    memset(plgctx, 0, sizeof(*plgctx));
    plgctx->magic = PKINIT_CTX_MAGIC;

    plgctx->realmname = strdup(realmname);
    if (plgctx->realmname == NULL)
        goto errout;
    plgctx->realmname_len = strlen(plgctx->realmname);

    retval = pkinit_init_plg_crypto(&plgctx->cryptoctx);
    if (retval)
        goto errout;

    retval = pkinit_init_plg_opts(&plgctx->opts);
    if (retval)
        goto errout;

    retval = pkinit_init_identity_crypto(&plgctx->idctx);
    if (retval)
        goto errout;

    retval = pkinit_init_identity_opts(&plgctx->idopts);
    if (retval)
        goto errout;

    retval = pkinit_init_kdc_profile(context, plgctx);
    if (retval)
        goto errout;

    retval = pkinit_identity_initialize(context, plgctx->cryptoctx, NULL,
                                        plgctx->idopts, plgctx->idctx,
                                        NULL, NULL, NULL);
    if (retval)
        goto errout;
    retval = pkinit_identity_prompt(context, plgctx->cryptoctx, NULL,
                                    plgctx->idopts, plgctx->idctx,
                                    NULL, NULL, 0, NULL);
    if (retval)
        goto errout;

    pkiDebug("%s: returning context at %p for realm '%s'\n",
             __FUNCTION__, plgctx, realmname);
    *pplgctx = plgctx;
    retval = 0;

errout:
    if (retval)
        pkinit_server_plugin_fini_realm(context, plgctx);

    return retval;
}

static krb5_error_code
pkinit_san_authorize(krb5_context context, krb5_certauth_moddata moddata,
                     const uint8_t *cert, size_t cert_len,
                     krb5_const_principal princ, const void *opts,
                     const struct _krb5_db_entry_new *db_entry,
                     char ***authinds_out)
{
    krb5_error_code ret;
    int valid_san;
    const struct certauth_req_opts *req_opts = opts;

    *authinds_out = NULL;

    ret = verify_client_san(context, req_opts->plgctx, req_opts->reqctx,
                            req_opts->cb, req_opts->rock, princ, &valid_san);
    if (ret == ENOENT)
        return KRB5_PLUGIN_NO_HANDLE;
    else if (ret)
        return ret;

    if (!valid_san) {
        TRACE_PKINIT_SERVER_SAN_REJECT(context);
        return KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
    }

    return 0;
}

static krb5_error_code
pkinit_eku_authorize(krb5_context context, krb5_certauth_moddata moddata,
                     const uint8_t *cert, size_t cert_len,
                     krb5_const_principal princ, const void *opts,
                     const struct _krb5_db_entry_new *db_entry,
                     char ***authinds_out)
{
    krb5_error_code ret;
    int valid_eku;
    const struct certauth_req_opts *req_opts = opts;

    *authinds_out = NULL;

    /* Verify the client EKU. */
    ret = verify_client_eku(context, req_opts->plgctx, req_opts->reqctx,
                            &valid_eku);
    if (ret)
        return ret;

    if (!valid_eku) {
        TRACE_PKINIT_SERVER_EKU_REJECT(context);
        return KRB5KDC_ERR_INCONSISTENT_KEY_PURPOSE;
    }

    return KRB5_PLUGIN_NO_HANDLE;
}

static krb5_error_code
certauth_pkinit_san_initvt(krb5_context context, int maj_ver, int min_ver,
                           krb5_plugin_vtable vtable)
{
    krb5_certauth_vtable vt;

    if (maj_ver != 1)
        return KRB5_PLUGIN_VER_NOTSUPP;
    vt = (krb5_certauth_vtable)vtable;
    vt->name = "pkinit_san";
    vt->authorize = pkinit_san_authorize;
    return 0;
}

static krb5_error_code
certauth_pkinit_eku_initvt(krb5_context context, int maj_ver, int min_ver,
                           krb5_plugin_vtable vtable)
{
    krb5_certauth_vtable vt;

    if (maj_ver != 1)
        return KRB5_PLUGIN_VER_NOTSUPP;
    vt = (krb5_certauth_vtable)vtable;
    vt->name = "pkinit_eku";
    vt->authorize = pkinit_eku_authorize;
    return 0;
}

/*
 * Do certificate auth based on a match expression in the pkinit_cert_match
 * attribute string.  An expression should be in the same form as those used
 * for the pkinit_cert_match configuration option.
 */
static krb5_error_code
dbmatch_authorize(krb5_context context, krb5_certauth_moddata moddata,
                  const uint8_t *cert, size_t cert_len,
                  krb5_const_principal princ, const void *opts,
                  const struct _krb5_db_entry_new *db_entry,
                  char ***authinds_out)
{
    krb5_error_code ret;
    const struct certauth_req_opts *req_opts = opts;
    char *pattern;
    krb5_boolean matched;

    *authinds_out = NULL;

    /* Fetch the matching pattern.  Pass if it isn't specified. */
    ret = req_opts->cb->get_string(context, req_opts->rock,
                                   "pkinit_cert_match", &pattern);
    if (ret)
        return ret;
    if (pattern == NULL)
        return KRB5_PLUGIN_NO_HANDLE;

    /* Check the certificate against the match expression. */
    ret = pkinit_client_cert_match(context, req_opts->plgctx->cryptoctx,
                                   req_opts->reqctx->cryptoctx, pattern,
                                   &matched);
    req_opts->cb->free_string(context, req_opts->rock, pattern);
    if (ret)
        return ret;
    return matched ? 0 : KRB5KDC_ERR_CERTIFICATE_MISMATCH;
}

static krb5_error_code
certauth_dbmatch_initvt(krb5_context context, int maj_ver, int min_ver,
                        krb5_plugin_vtable vtable)
{
    krb5_certauth_vtable vt;

    if (maj_ver != 1)
        return KRB5_PLUGIN_VER_NOTSUPP;
    vt = (krb5_certauth_vtable)vtable;
    vt->name = "dbmatch";
    vt->authorize = dbmatch_authorize;
    return 0;
}

static krb5_error_code
load_certauth_plugins(krb5_context context, certauth_handle **handle_out)
{
    krb5_error_code ret;
    krb5_plugin_initvt_fn *modules = NULL, *mod;
    certauth_handle *list = NULL, h;
    size_t count;

    /* Register the builtin modules. */
    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CERTAUTH,
                             "pkinit_san", certauth_pkinit_san_initvt);
    if (ret)
        goto cleanup;

    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CERTAUTH,
                             "pkinit_eku", certauth_pkinit_eku_initvt);
    if (ret)
        goto cleanup;

    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CERTAUTH, "dbmatch",
                             certauth_dbmatch_initvt);
    if (ret)
        goto cleanup;

    ret = k5_plugin_load_all(context, PLUGIN_INTERFACE_CERTAUTH, &modules);
    if (ret)
        goto cleanup;

    /* Allocate handle list. */
    for (count = 0; modules[count]; count++);
    list = k5calloc(count + 1, sizeof(*list), &ret);
    if (list == NULL)
        goto cleanup;

    /* Initialize each module, ignoring ones that fail. */
    count = 0;
    for (mod = modules; *mod != NULL; mod++) {
        h = k5calloc(1, sizeof(*h), &ret);
        if (h == NULL)
            goto cleanup;

        ret = (*mod)(context, 1, 1, (krb5_plugin_vtable)&h->vt);
        if (ret) {
            TRACE_CERTAUTH_VTINIT_FAIL(context, ret);
            free(h);
            continue;
        }
        h->moddata = NULL;
        if (h->vt.init != NULL) {
            ret = h->vt.init(context, &h->moddata);
            if (ret) {
                TRACE_CERTAUTH_INIT_FAIL(context, h->vt.name, ret);
                free(h);
                continue;
            }
        }
        list[count++] = h;
        list[count] = NULL;
    }
    list[count] = NULL;

    ret = 0;
    *handle_out = list;
    list = NULL;

cleanup:
    k5_plugin_free_modules(context, modules);
    free_certauth_handles(context, list);
    return ret;
}

static int
pkinit_server_plugin_init(krb5_context context,
                          krb5_kdcpreauth_moddata *moddata_out,
                          const char **realmnames)
{
    krb5_error_code retval = ENOMEM;
    pkinit_kdc_context plgctx, *realm_contexts = NULL;
    certauth_handle *certauth_modules = NULL;
    krb5_kdcpreauth_moddata moddata;
    size_t  i, j;
    size_t numrealms;

    retval = pkinit_accessor_init();
    if (retval)
        return retval;

    /* Determine how many realms we may need to support */
    for (i = 0; realmnames[i] != NULL; i++) {};
    numrealms = i;

    realm_contexts = calloc(numrealms+1, sizeof(pkinit_kdc_context));
    if (realm_contexts == NULL)
        return ENOMEM;

    for (i = 0, j = 0; i < numrealms; i++) {
        TRACE_PKINIT_SERVER_INIT_REALM(context, realmnames[i]);
        krb5_clear_error_message(context);
        retval = pkinit_server_plugin_init_realm(context, realmnames[i],
                                                 &plgctx);
        if (retval)
            TRACE_PKINIT_SERVER_INIT_FAIL(context, realmnames[i], retval);
        else
            realm_contexts[j++] = plgctx;
    }

    if (j == 0) {
        if (numrealms == 1) {
            k5_prependmsg(context, retval, "PKINIT initialization failed");
        } else {
            retval = EINVAL;
            k5_setmsg(context, retval,
                      _("No realms configured correctly for pkinit support"));
        }
        goto errout;
    }

    retval = load_certauth_plugins(context, &certauth_modules);
    if (retval)
        goto errout;

    moddata = k5calloc(1, sizeof(*moddata), &retval);
    if (moddata == NULL)
        goto errout;
    moddata->realm_contexts = realm_contexts;
    moddata->certauth_modules = certauth_modules;
    *moddata_out = moddata;
    pkiDebug("%s: returning context at %p\n", __FUNCTION__, moddata);
    return 0;

errout:
    free_realm_contexts(context, realm_contexts);
    free_certauth_handles(context, certauth_modules);
    return retval;
}

static void
pkinit_server_plugin_fini_realm(krb5_context context, pkinit_kdc_context plgctx)
{
    char **sp;

    if (plgctx == NULL)
        return;

    pkinit_fini_kdc_profile(context, plgctx);
    pkinit_fini_identity_opts(plgctx->idopts);
    pkinit_fini_identity_crypto(plgctx->idctx);
    pkinit_fini_plg_crypto(plgctx->cryptoctx);
    pkinit_fini_plg_opts(plgctx->opts);
    for (sp = plgctx->auth_indicators; sp != NULL && *sp != NULL; sp++)
        free(*sp);
    free(plgctx->auth_indicators);
    free(plgctx->realmname);
    free(plgctx);
}

static void
pkinit_server_plugin_fini(krb5_context context,
                          krb5_kdcpreauth_moddata moddata)
{
    if (moddata == NULL)
        return;
    free_realm_contexts(context, moddata->realm_contexts);
    free_certauth_handles(context, moddata->certauth_modules);
    free(moddata);
}

static krb5_error_code
pkinit_init_kdc_req_context(krb5_context context, pkinit_kdc_req_context *ctx)
{
    krb5_error_code retval = ENOMEM;
    pkinit_kdc_req_context reqctx = NULL;

    reqctx = malloc(sizeof(*reqctx));
    if (reqctx == NULL)
        return retval;
    memset(reqctx, 0, sizeof(*reqctx));
    reqctx->magic = PKINIT_CTX_MAGIC;

    retval = pkinit_init_req_crypto(&reqctx->cryptoctx);
    if (retval)
        goto cleanup;
    reqctx->rcv_auth_pack = NULL;

    pkiDebug("%s: returning reqctx at %p\n", __FUNCTION__, reqctx);
    *ctx = reqctx;
    retval = 0;
cleanup:
    if (retval)
        pkinit_fini_kdc_req_context(context, reqctx);

    return retval;
}

static void
pkinit_fini_kdc_req_context(krb5_context context, void *ctx)
{
    pkinit_kdc_req_context reqctx = (pkinit_kdc_req_context)ctx;

    if (reqctx == NULL || reqctx->magic != PKINIT_CTX_MAGIC) {
        pkiDebug("pkinit_fini_kdc_req_context: got bad reqctx (%p)!\n", reqctx);
        return;
    }
    pkiDebug("%s: freeing reqctx at %p\n", __FUNCTION__, reqctx);

    pkinit_fini_req_crypto(reqctx->cryptoctx);
    if (reqctx->rcv_auth_pack != NULL)
        free_krb5_auth_pack(&reqctx->rcv_auth_pack);

    free(reqctx);
}

krb5_error_code
kdcpreauth_pkinit_initvt(krb5_context context, int maj_ver, int min_ver,
                         krb5_plugin_vtable vtable);

krb5_error_code
kdcpreauth_pkinit_initvt(krb5_context context, int maj_ver, int min_ver,
                         krb5_plugin_vtable vtable)
{
    krb5_kdcpreauth_vtable vt;

    if (maj_ver != 1)
        return KRB5_PLUGIN_VER_NOTSUPP;
    vt = (krb5_kdcpreauth_vtable)vtable;
    vt->name = "pkinit";
    vt->pa_type_list = supported_server_pa_types;
    vt->init = pkinit_server_plugin_init;
    vt->fini = pkinit_server_plugin_fini;
    vt->flags = pkinit_server_get_flags;
    vt->edata = pkinit_server_get_edata;
    vt->verify = pkinit_server_verify_padata;
    vt->return_padata = pkinit_server_return_padata;
    return 0;
}
