/*
 * Copyright (c) 2006 - 2019 Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include "kdc_locl.h"
#include <hex.h>
#include <rfc2459_asn1.h>
#include <hx509.h>
#include <hx509_err.h>
#include <kx509_err.h>

#include <stdarg.h>

/*
 * This file implements the kx509 service.
 *
 * The protocol, its shortcomings, and its future are described in
 * lib/krb5/hx509.c.  See also lib/asn1/kx509.asn1.
 *
 * The service handles requests, decides whether to issue a certificate, and
 * does so by populating a "template" to generate a TBSCertificate and signing
 * it with a configured CA issuer certificate and private key.  See ca.c for
 * details.
 *
 * A "template" is a Certificate that has ${variable} references in its
 * subjectName, and may have EKUs.
 *
 * Some SANs may be included in issued certificates.  See below.
 *
 * Besides future protocol improvements described in lib/krb5/hx509.c, here is
 * a list of KDC functionality we'd like to add:
 *
 *  - support templates as strings (rather than filenames) in configuration?
 *  - lookup an hx509 template for the client principal in its HDB entry?
 *  - lookup subjectName, SANs for a principal in its HDB entry
 *  - lookup a host-based client principal's HDB entry and add its canonical
 *    name / aliases as dNSName SANs
 *    (this would have to be if requested by the client, perhaps; see
 *     commentary about the protocol in lib/krb5/kx509.c)
 *  - add code to build a template on the fly
 *
 *    (just SANs, with empty subjectName?
 *     or
 *     CN=component0,CN=component1,..,CN=componentN,DC=<from-REALM>
 *     and set KU and EKUs)
 *
 * Processing begins in _kdc_do_kx509().
 *
 * The sequence of events in _kdc_do_kx509() is:
 *
 *  - parse outer request
 *  - authenticate request
 *  - extract CSR and AP-REQ Authenticator authz-data elements
 *  - characterize request as one of
 *     - default client cert req (no cert exts requested, client user princ)
 *     - default server cert req (no cert exts requested, client service princ)
 *     - client cert req (cert exts requested denoting client use)
 *     - server cert req (cert exts requested denoting server use)
 *     - mixed  cert req (cert exts requested denoting client and server use)
 *  - authorize request based only on the request's details
 *     - there is a default authorizer, and a plugin authorizer
 *  - get configuration sub-tree corresponding to the request as characterized
 *     - missing configuration sub-tree -> reject (we have multiple ways to
 *       express "no")
 *  - get common config params from that sub-tree
 *  - set TBS template and details from CSR and such
 *  - issue certificate by signing TBS
 */

#ifdef KX509

static const unsigned char version_2_0[4] = {0 , 0, 2, 0};

/*
 * Taste the request to see if it's a kx509 request.
 */
krb5_error_code
_kdc_try_kx509_request(kx509_req_context r)
{
    const unsigned char *p = (const void *)(uintptr_t)r->request.data;
    size_t len = r->request.length;
    size_t sz;

    if (len < sizeof(version_2_0))
        return -1;
    if (memcmp(version_2_0, p, sizeof(version_2_0)) != 0)
        return -1;
    p += sizeof(version_2_0);
    len -= sizeof(version_2_0);
    if (len == 0)
        return -1;
    memset(&r->req, 0, sizeof(r->req));
    return decode_Kx509Request(p, len, &r->req, &sz);
}

static krb5_boolean
get_bool_param(krb5_context context,
               krb5_boolean def,
               const char *crealm,
               const char *name)
{
    krb5_boolean global_default;

    global_default = krb5_config_get_bool_default(context, NULL, def, "kdc",
                                                  name, NULL);
    if (!crealm)
        return global_default;
    return krb5_config_get_bool_default(context, NULL, global_default,
                                        "kdc", "realms", crealm, name, NULL);
}

/*
 * Verify the HMAC in the request.
 */
static krb5_error_code
verify_req_hash(krb5_context context,
                const Kx509Request *req,
                krb5_keyblock *key)
{
    unsigned char digest[SHA_DIGEST_LENGTH];
    EVP_MAC_CTX *ctx = NULL;
    size_t maclen = SHA_DIGEST_LENGTH;
    int ret;

    if (req->pk_hash.length != sizeof(digest)) {
        krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED,
                               "pk-hash has wrong length: %lu",
                               (unsigned long)req->pk_hash.length);
        return KRB5KDC_ERR_PREAUTH_FAILED;
    }

    ret = _krb5_hmac_start_ossl(context, key->keyvalue.data, key->keyvalue.length,
                                EVP_sha1(), &ctx);
    if (ret == 0 &&
        EVP_MAC_update(ctx, version_2_0, sizeof(version_2_0)) != 1) {
        ret = KRB5KDC_ERR_PREAUTH_FAILED;
    }
    if (ret == 0 && req->pk_key.length) {
        if (EVP_MAC_update(ctx, req->pk_key.data, req->pk_key.length) != 1)
            ret = KRB5KDC_ERR_PREAUTH_FAILED;
    } else if (ret == 0) {
        if (EVP_MAC_update(ctx, req->authenticator.data,
                           req->authenticator.length) != 1)
            ret = KRB5KDC_ERR_PREAUTH_FAILED;
    }
    if (ret == 0 &&
        EVP_MAC_final(ctx, digest, &maclen, maclen) != 1)
        ret = KRB5KDC_ERR_PREAUTH_FAILED;
    EVP_MAC_CTX_free(ctx);
    if (ret) {
        krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED,
                               "kx509 could not compute HMAC");
        return ret;
    }

    if (ct_memcmp(req->pk_hash.data, digest, sizeof(digest)) != 0) {
        krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED,
                               "kx509 request MAC mismatch");
        return KRB5KDC_ERR_PREAUTH_FAILED;
    }
    return 0;
}

/*
 * Set the HMAC in the response.
 */
static krb5_error_code
calculate_reply_hash(krb5_context context,
                     krb5_keyblock *key,
                     Kx509Response *rep)
{
    krb5_error_code ret;
    EVP_MAC_CTX *ctx = NULL;
    size_t maclen = SHA_DIGEST_LENGTH;

    ret = _krb5_hmac_start_ossl(context, key->keyvalue.data, key->keyvalue.length,
                                EVP_sha1(), &ctx);
    if (ret)
        ret = krb5_enomem(context);
    if (ret == 0)
        ret = krb5_data_alloc(rep->hash, SHA_DIGEST_LENGTH);
    if (ret == 0 &&
        EVP_MAC_update(ctx, version_2_0, sizeof(version_2_0)) != 1)
        ret = EINVAL;
    if (ret == 0) {
        int32_t t = rep->error_code;
        unsigned char encint[sizeof(t) + 1];
        size_t k;

        /*
         * RFC6717 says this about how the error-code is included in the HMAC:
         *
         *  o DER representation of the error-code exclusive of the tag and
         *    length, if it is present.
         *
         * So we use der_put_integer(), which encodes from the right.
         *
         * RFC6717 does not constrain the error-code's range.  We assume it to
         * be a 32-bit, signed integer, for which we'll need no more than 5
         * bytes.
         */
        ret = der_put_integer(&encint[sizeof(encint) - 1],
                              sizeof(encint), &t, &k);
        if (ret == 0 &&
            EVP_MAC_update(ctx, &encint[sizeof(encint)] - k, k) != 1)
            ret = EINVAL;
    }
    if (ret == 0 && rep->certificate &&
        EVP_MAC_update(ctx, rep->certificate->data,
                       rep->certificate->length) != 1)
        ret = EINVAL;
    if (ret == 0 && rep->e_text && 
        EVP_MAC_update(ctx, (unsigned char *)*rep->e_text,
                       strlen(*rep->e_text)) != 1)
        ret = EINVAL;

    if (ret == 0 &&
        EVP_MAC_final(ctx, rep->hash->data, &maclen, maclen) != 1)
        ret = EINVAL;
    EVP_MAC_CTX_free(ctx);

    return 0;
}

static void
frees(char **s)
{
    free(*s);
    *s = NULL;
}

/* Check that a krbtgt's second component is a local realm */
static krb5_error_code
is_local_realm(krb5_context context,
               kx509_req_context reqctx,
               const char *realm)
{
    krb5_error_code ret;
    krb5_principal tgs;
    HDB *db;
    hdb_entry *ent = NULL;

    ret = krb5_make_principal(context, &tgs, realm, KRB5_TGS_NAME, realm,
                              NULL);
    if (ret)
        return ret;
    if (ret == 0)
        ret = _kdc_db_fetch(context, reqctx->config, tgs, HDB_F_GET_KRBTGT,
                            NULL, &db, &ent);
    if (ent)
        _kdc_free_ent(context, db, ent);
    krb5_free_principal(context, tgs);
    if (ret == HDB_ERR_NOENTRY || ret == HDB_ERR_NOT_FOUND_HERE)
        return KRB5KRB_AP_ERR_NOT_US;
    return ret;
}

/*
 * Since we're using the HDB as a keytab we have to check that the client used
 * an acceptable name for the kx509 service.
 *
 * We accept two names: kca_service/hostname and krbtgt/REALM.
 *
 * We allow cross-realm requests.
 *
 *     Maybe x-realm support should be configurable.  Requiring INITIAL tickets
 *     does NOT preclude x-realm support!  (Cross-realm TGTs can be INITIAL.)
 *
 *     Support for specific client realms is configurable by configuring issuer
 *     credentials and TBS templates on a per-realm basis and configuring no
 *     default.  But maybe we should have an explicit configuration parameter
 *     to enable support for clients from different realms than the service.
 */
static krb5_error_code
kdc_kx509_verify_service_principal(krb5_context context,
                                   kx509_req_context reqctx,
                                   krb5_principal sprincipal)
{
    krb5_error_code ret = 0;
    krb5_principal principal = NULL;
    char *expected = NULL;
    char localhost[MAXHOSTNAMELEN];

    if (krb5_principal_get_num_comp(context, sprincipal) != 2)
        goto err;

    /* Check if sprincipal is a krbtgt/REALM name */
    if (strcmp(krb5_principal_get_comp_string(context, sprincipal, 0),
               KRB5_TGS_NAME) == 0) {
        const char *r = krb5_principal_get_comp_string(context, sprincipal, 1);
        if ((ret = is_local_realm(context, reqctx, r)))
            kdc_audit_addreason((kdc_request_t)reqctx,
                                "Client used wrong krbtgt for kx509");
        goto out;
    }

    /* Must be hostbased kca_service name then */
    ret = gethostname(localhost, sizeof(localhost) - 1);
    if (ret != 0) {
        ret = errno;
        kdc_log(context, reqctx->config, 0, "Failed to get local hostname");
        kdc_audit_addreason((kdc_request_t)reqctx,
                            "Failed to get local hostname");
        return ret;
    }
    localhost[sizeof(localhost) - 1] = '\0';

    ret = krb5_make_principal(context, &principal, "", "kca_service",
                              localhost, NULL);
    if (ret)
        goto out;

    if (krb5_principal_compare_any_realm(context, sprincipal, principal))
        goto out;       /* found a match */

err:
    ret = krb5_unparse_name(context, sprincipal, &expected);
    if (ret)
        goto out;

    ret = KRB5KDC_ERR_SERVER_NOMATCH;
    kdc_audit_addreason((kdc_request_t)reqctx, "Client used wrong kx509 "
                        "service principal (expected %s)", expected);

out:
    krb5_xfree(expected);
    krb5_free_principal(context, principal);

    return ret;
}

static krb5_error_code
encode_reply(krb5_context context,
             kx509_req_context reqctx,
             Kx509Response *r)
{
    krb5_error_code ret;
    krb5_data data;
    size_t size;

    reqctx->reply->data = NULL;
    reqctx->reply->length = 0;
    ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, r, &size, ret);
    if (ret)
        return ret;
    if (size != data.length)
        krb5_abortx(context, "ASN1 internal error");

    ret = krb5_data_alloc(reqctx->reply, data.length + sizeof(version_2_0));
    if (ret == 0) {
        memcpy(reqctx->reply->data, version_2_0, sizeof(version_2_0));
        memcpy(((unsigned char *)reqctx->reply->data) + sizeof(version_2_0),
               data.data, data.length);
    }
    free(data.data);
    return ret;
}

/* Make an error response, and log the error message as well */
static krb5_error_code
mk_error_response(krb5_context context,
                  kx509_req_context reqctx,
                  int level,
                  int32_t code,
                  const char *fmt,
                  ...)
{
    krb5_error_code ret = code;
    krb5_error_code ret2;
    Kx509Response rep;
    const char *msg;
    char *freeme0 = NULL;
    char *freeme1 = NULL;
    va_list ap;

    if (code != 0) {
        /* Log errors where _kdc_audit_trail() is not enough */
        if (code == ENOMEM)
            level = 0;
        if (level < 3) {
            va_start(ap, fmt);
            kdc_vlog(context, reqctx->config, level, fmt, ap);
            va_end(ap);
        }

        va_start(ap, fmt);
        kdc_audit_vaddreason((kdc_request_t)reqctx, fmt, ap);
        va_end(ap);
    }

    if (!reqctx->config->enable_kx509)
        code = KRB5KDC_ERR_POLICY;

    /* Make sure we only send RFC4120 and friends wire protocol error codes */
    if (code) {
        if (code == KX509_ERR_NONE) {
            code = 0;
        } else if (code > KX509_ERR_NONE && code <= KX509_ERR_SRV_OVERLOADED) {
            code -= KX509_ERR_NONE;
        } else {
            if (code < KRB5KDC_ERR_NONE || code >= KRB5_ERR_RCSID)
                code = KRB5KRB_ERR_GENERIC;
            code -= KRB5KDC_ERR_NONE;
            code += kx509_krb5_error_base;
        }
    }

    va_start(ap, fmt);
    if (vasprintf(&freeme0, fmt, ap) == -1 || freeme0 == NULL)
        msg = "Could not format error message (out of memory)";
    else
        msg = freeme0;
    va_end(ap);

    if (!reqctx->config->enable_kx509 &&
        asprintf(&freeme1, "kx509 service is disabled (%s)", msg) > -1 &&
        freeme1 != NULL) {
        msg = freeme1;
    }

    rep.hash = NULL;
    rep.certificate = NULL;
    rep.error_code = code;
    if (ALLOC(rep.e_text))
        *rep.e_text = (void *)(uintptr_t)msg;

    if (reqctx->key) {
        if (ALLOC(rep.hash) != NULL &&
            calculate_reply_hash(context, reqctx->key, &rep)) {
            free(rep.hash);
            rep.hash = NULL;
        }
    }

    if ((ret2 = encode_reply(context, reqctx, &rep)))
        ret = ret2;
    if (rep.hash)
        krb5_data_free(rep.hash);
    free(rep.e_text);
    free(rep.hash);
    free(freeme0);
    free(freeme1);
    return ret;
}

/* Wrap a bare public (RSA) key with a CSR (not signed it, since we can't) */
static krb5_error_code
make_csr(krb5_context context, kx509_req_context reqctx, krb5_data *key)
{
    krb5_error_code ret;
    SubjectPublicKeyInfo spki;
    heim_any any;

    ret = hx509_request_init(context->hx509ctx, &reqctx->csr);
    if (ret)
        return ret;

    memset(&spki, 0, sizeof(spki));
    spki.subjectPublicKey.data = key->data;
    spki.subjectPublicKey.length = key->length * 8;

    ret = der_copy_oid(&asn1_oid_id_pkcs1_rsaEncryption,
                       &spki.algorithm.algorithm);

    /* copied read-only by hx509_request_set_SubjectPublicKeyInfo */
    any.data = rk_UNCONST("\x05\x00");
    any.length = 2;
    spki.algorithm.parameters = &any;

    if (ret == 0)
        ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx,
                                                     reqctx->csr, &spki);
    der_free_oid(&spki.algorithm.algorithm);
    if (ret)
        hx509_request_free(&reqctx->csr);

    /*
     * TODO: Move a lot of the templating stuff here so we can let clients
     *       leave out extensions they don't want.
     */
    return ret;
}

/* Update a CSR with desired Certificate Extensions */
static krb5_error_code
update_csr(krb5_context context, kx509_req_context reqctx, Extensions *exts)
{
    krb5_error_code ret = 0;
    size_t i, k;

    if (exts == NULL)
        return 0;

    for (i = 0; ret == 0 && i < exts->len; i++) {
        Extension *e = &exts->val[i];

        if (der_heim_oid_cmp(&e->extnID, &asn1_oid_id_x509_ce_keyUsage) == 0) {
            KeyUsage ku;

            ret = decode_KeyUsage(e->extnValue.data, e->extnValue.length, &ku,
                                  NULL);
            if (ret)
                return ret;
            ret = hx509_request_set_ku(context->hx509ctx, reqctx->csr, ku);
        } else if (der_heim_oid_cmp(&e->extnID,
                                    &asn1_oid_id_x509_ce_extKeyUsage) == 0) {
            ExtKeyUsage eku;

            ret = decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length,
                                     &eku, NULL);
            for (k = 0; ret == 0 && k < eku.len; k++) {
                ret = hx509_request_add_eku(context->hx509ctx, reqctx->csr,
                                            &eku.val[k]);
            }
            free_ExtKeyUsage(&eku);
        } else if (der_heim_oid_cmp(&e->extnID,
                                    &asn1_oid_id_x509_ce_subjectAltName) == 0) {
            GeneralNames san;

            ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length,
                                      &san, NULL);
            for (k = 0; ret == 0 && k < san.len; k++) {
                ret = hx509_request_add_GeneralName(context->hx509ctx,
                                                    reqctx->csr, &san.val[k]);
            }
            free_GeneralNames(&san);
        }
    }
    if (ret) {
	const char *emsg = krb5_get_error_message(context, ret);
        kdc_log(context, reqctx->config, 1,
                "Error handling requested extensions: %s", emsg);
        kdc_audit_addreason((kdc_request_t)reqctx,
                            "Error handling requested extensions: %s",
                            emsg);
	krb5_free_error_message(context, emsg);
    }
    return ret;
}


/*
 * Parse the `pk_key' from the request as a CSR or raw public key, and if the
 * latter, wrap it in a non-signed CSR.
 */
static krb5_error_code
get_csr(krb5_context context, kx509_req_context reqctx)
{
    krb5_error_code ret;
    RSAPublicKey rsapkey;
    heim_octet_string pk_key = reqctx->req.pk_key;
    size_t size;

    ret = decode_Kx509CSRPlus(pk_key.data, pk_key.length, &reqctx->csr_plus,
                              &size);
    if (ret == 0) {
        reqctx->have_csr = 1;
        reqctx->send_chain = 1;

        /* Parse CSR */
        ret = hx509_request_parse_der(context->hx509ctx, &reqctx->csr_plus.csr,
                                      &reqctx->csr);
        /*
         * Handle any additional Certificate Extensions requested out of band
         * of the CSR.
         */
        if (ret == 0)
            return update_csr(context, reqctx, reqctx->csr_plus.exts);
        kdc_audit_addreason((kdc_request_t)reqctx, "Invalid CSR");
        return ret;
    }
    reqctx->send_chain = 0;
    reqctx->have_csr = 0;

    /* Check if proof of possession is required by configuration */
    if (!get_bool_param(context, FALSE, reqctx->realm, "require_csr")) {
        kdc_audit_addreason((kdc_request_t)reqctx,
                            "CSRs required but client did not send one");
        krb5_set_error_message(context, KX509_STATUS_CLIENT_USE_CSR,
                               "CSRs required but kx509 client did not send "
                               "one");
        return KX509_STATUS_CLIENT_USE_CSR;
    }

    /* Attempt to decode pk_key as RSAPublicKey */
    ret = decode_RSAPublicKey(reqctx->req.pk_key.data,
                              reqctx->req.pk_key.length,
                              &rsapkey, &size);
    free_RSAPublicKey(&rsapkey);
    if (ret == 0 && size == reqctx->req.pk_key.length)
        return make_csr(context, reqctx, &pk_key); /* Make pretend CSR */

    /* Not an RSAPublicKey or garbage follows it */
    if (ret == 0) {
        ret = KRB5KDC_ERR_NULL_KEY;
        kdc_audit_addreason((kdc_request_t)reqctx,
                            "Request has garbage after key");
        krb5_set_error_message(context, ret, "Request has garbage after key");
        return ret;
    }

    kdc_audit_addreason((kdc_request_t)reqctx,
                        "Could not decode CSR or RSA subject public key");
    krb5_set_error_message(context, ret,
                           "Could not decode CSR or RSA subject public key");
    return ret;
}

/*
 * Host-based principal _clients_ might ask for a cert for their host -- but
 * which services are permitted to do that?  This function answers that
 * question.
 */
static int
check_authz_svc_ok(krb5_context context, const char *svc)
{
    const char *def[] = { "host", "HTTP", 0 };
    const char * const *svcs;
    char **strs;

    strs = krb5_config_get_strings(context, NULL, "kdc",
                                   "kx509_permitted_hostbased_services", NULL);
    for (svcs = strs ? (const char * const *)strs : def; svcs[0]; svcs++) {
        if (strcmp(svcs[0], svc) == 0) {
            krb5_config_free_strings(strs);
            return 1;
        }
    }
    krb5_config_free_strings(strs);
    return 0;
}

static krb5_error_code
check_authz(krb5_context context,
            kx509_req_context reqctx,
            krb5_principal cprincipal)
{
    krb5_error_code ret;
    const char *comp0 = krb5_principal_get_comp_string(context, cprincipal, 0);
    const char *comp1 = krb5_principal_get_comp_string(context, cprincipal, 1);
    unsigned int ncomp = krb5_principal_get_num_comp(context, cprincipal);
    hx509_san_type san_type;
    KeyUsage ku, ku_allowed;
    size_t i;
    const heim_oid *eku_whitelist[] = {
        &asn1_oid_id_pkix_kp_serverAuth,
        &asn1_oid_id_pkix_kp_clientAuth,
        &asn1_oid_id_pkekuoid,
        &asn1_oid_id_pkinit_ms_eku
    };
    char *cprinc = NULL;
    char *s = NULL;

    /*
     * In the no-CSR case we'll derive cert contents from client name and its
     * HDB entry -- authorization is implied.
     */
    if (!reqctx->have_csr)
        return 0;
    ret = kdc_authorize_csr(context, reqctx->config->app, reqctx->csr,
                            cprincipal);
    if (ret == 0) {
        kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE);

        ret = hx509_request_get_san(reqctx->csr, 0, &san_type, &s);
        if (ret == 0) {
            const char *san_type_s;

            /* This should be an hx509 function... */
            switch (san_type) {
            case HX509_SAN_TYPE_EMAIL: san_type_s = "rfc822Name"; break;
            case HX509_SAN_TYPE_DNSNAME: san_type_s = "dNSName"; break;
            case HX509_SAN_TYPE_DN: san_type_s = "DN"; break;
            case HX509_SAN_TYPE_REGISTERED_ID: san_type_s = "registeredID"; break;
            case HX509_SAN_TYPE_XMPP: san_type_s = "xMPPName"; break;
            case HX509_SAN_TYPE_PKINIT: san_type_s = "krb5PrincipalName"; break;
            case HX509_SAN_TYPE_MS_UPN: san_type_s = "ms-UPN"; break;
            default: san_type_s = "unknown"; break;
            }
            kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0_type", "%s",
                            san_type_s);
            kdc_audit_addkv((kdc_request_t)reqctx, 0, "san0", "%s", s);
        }
	frees(&s);
        ret = hx509_request_get_eku(reqctx->csr, 0, &s);
        if (ret == 0)
            kdc_audit_addkv((kdc_request_t)reqctx, 0, "eku0", "%s", s);
	free(s);
        return 0;
    }
    if (ret != KRB5_PLUGIN_NO_HANDLE) {
        kdc_audit_addreason((kdc_request_t)reqctx,
                             "Requested extensions rejected by plugin");
        return ret;
    }

    /* Default authz */
    if ((ret = krb5_unparse_name(context, cprincipal, &cprinc)))
        return ret;

    for (i = 0; ret == 0; i++) {

        frees(&s);
        ret = hx509_request_get_san(reqctx->csr, i, &san_type, &s);
        if (ret)
            break;
        switch (san_type) {
        case HX509_SAN_TYPE_DNSNAME:
            if (ncomp != 2 || strcasecmp(comp1, s) != 0 ||
                strchr(s, '.') == NULL ||
                !check_authz_svc_ok(context, comp0)) {
                kdc_audit_addreason((kdc_request_t)reqctx,
                                    "Requested extensions rejected by "
                                    "default policy (dNSName SAN "
                                    "does not match client)");
                goto eacces;
            }
            break;
        case HX509_SAN_TYPE_PKINIT:
            if (strcmp(cprinc, s) != 0) {
                kdc_audit_addreason((kdc_request_t)reqctx,
                                    "Requested extensions rejected by "
                                    "default policy (PKINIT SAN "
                                    "does not match client)");
                goto eacces;
            }
            break;
        default:
            kdc_audit_addreason((kdc_request_t)reqctx,
                                "Requested extensions rejected by "
                                "default policy (non-default SAN "
                                "requested)");
            goto eacces;
        }
    }
    frees(&s);
    if (ret == HX509_NO_ITEM)
        ret = 0;
    if (ret)
        goto out;

    for (i = 0; ret == 0; i++) {
        heim_oid oid;
        size_t k;

        frees(&s);
        ret = hx509_request_get_eku(reqctx->csr, i, &s);
        if (ret)
            break;

        if ((ret = der_parse_heim_oid(s, ".", &oid))) {
            goto out;
        }
        for (k = 0; k < sizeof(eku_whitelist)/sizeof(eku_whitelist[0]); k++) {
            if (der_heim_oid_cmp(eku_whitelist[k], &oid) == 0)
                break;
        }
        der_free_oid(&oid);
        if (k == sizeof(eku_whitelist)/sizeof(eku_whitelist[0])) {
            kdc_audit_addreason((kdc_request_t)reqctx,
                                "Requested EKU rejected by default policy");
            goto eacces;
        }
    }
    if (ret == HX509_NO_ITEM)
        ret = 0;
    if (ret)
        goto out;

    memset(&ku_allowed, 0, sizeof(ku_allowed));
    ku_allowed.digitalSignature = 1;
    ku_allowed.nonRepudiation = 1;
    ret = hx509_request_get_ku(context->hx509ctx, reqctx->csr, &ku);
    if (ret)
        goto out;
    if (KeyUsage2int(ku) != (KeyUsage2int(ku) & KeyUsage2int(ku_allowed)))
        goto eacces;

    kdc_audit_setkv_bool((kdc_request_t)reqctx, "authorized", TRUE);
    free(cprinc);
    return 0;

eacces:
    ret = EACCES;
    goto out2;

out:
    /* XXX Display error code */
    kdc_audit_addreason((kdc_request_t)reqctx,
                         "Error handling requested extensions");
out2:
    free(cprinc);
    free(s);
    return ret;
}

static int
chain_add1_func(hx509_context context, void *d, hx509_cert c)
{
    heim_octet_string os;
    Certificates *cs = d;
    Certificate c2;
    int ret;

    ret = hx509_cert_binary(context, c, &os);
    if (ret)
        return ret;
    ret = decode_Certificate(os.data, os.length, &c2, NULL);
    der_free_octet_string(&os);
    if (ret)
        return ret;
    ret = add_Certificates(cs, &c2);
    free_Certificate(&c2);
    return ret;
}

static krb5_error_code
encode_cert_and_chain(hx509_context hx509ctx,
                      hx509_certs certs,
                      krb5_data *out)
{
    krb5_error_code ret;
    Certificates cs;
    size_t len;

    cs.len = 0;
    cs.val = 0;

    ret = hx509_certs_iter_f(hx509ctx, certs, chain_add1_func, &cs);
    if (ret == 0)
        ASN1_MALLOC_ENCODE(Certificates, out->data, out->length,
                           &cs, &len, ret);
    free_Certificates(&cs);
    return ret;
}

/*
 * Process a request, produce a reply.
 */

krb5_error_code
_kdc_do_kx509(kx509_req_context r)
{
    krb5_error_code ret = 0;
    krb5_ticket *ticket = NULL;
    krb5_flags ap_req_options;
    krb5_principal cprincipal = NULL;
    krb5_principal sprincipal = NULL;
    krb5_keytab id = NULL;
    Kx509Response rep;
    hx509_certs certs = NULL;
    int is_probe = 0;

    r->csr_plus.csr.data = NULL;
    r->csr_plus.exts = NULL;
    r->sname = NULL;
    r->cname = NULL;
    r->realm = NULL;
    r->key = NULL;
    r->csr = NULL;
    r->ac = NULL;

    /*
     * In order to support authenticated error messages we defer checking
     * whether the kx509 service is enabled until after accepting the AP-REQ.
     */

    krb5_data_zero(r->reply);
    memset(&rep, 0, sizeof(rep));

    if (r->req.authenticator.length == 0) {
        /*
         * Unauthenticated kx509 service availability probe.
         *
         * mk_error_response() will check whether the service is enabled and
         * possibly change the error code and message.
         */
        is_probe = 1;
        kdc_audit_addkv((kdc_request_t)r, 0, "probe", "unauthenticated");
        ret = mk_error_response(r->context, r, 4, 0,
                                "kx509 service is available");
        goto out;
    }

    /* Authenticate the request (consume the AP-REQ) */
    ret = krb5_kt_resolve(r->context, "HDBGET:", &id);
    if (ret) {
	const char *msg = krb5_get_error_message(r->context, ret);
        ret = mk_error_response(r->context, r, 1,
                                KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN,
                                "Can't open HDB/keytab for kx509: %s",
				msg);
	krb5_free_error_message(r->context, msg);
        goto out;
    }

    ret = krb5_rd_req(r->context,
                      &r->ac,
                      &r->req.authenticator,
                      NULL,
                      id,
                      &ap_req_options,
                      &ticket);
    if (ret == 0)
        ret = krb5_auth_con_getkey(r->context, r->ac, &r->key);
    if (ret == 0 && r->key == NULL)
        ret = KRB5KDC_ERR_NULL_KEY;
    /*
     * Provided we got the session key, errors past this point will be
     * authenticated.
     */
    if (ret == 0)
        ret = krb5_ticket_get_client(r->context, ticket, &cprincipal);

    /* Optional: check if Ticket is INITIAL */
    if (ret == 0 &&
        !ticket->ticket.flags.initial &&
        !get_bool_param(r->context, TRUE,
                        krb5_principal_get_realm(r->context, cprincipal),
                        "require_initial_kca_tickets")) {
        ret = mk_error_response(r->context, r, 4, KRB5KDC_ERR_POLICY,
                                "Client used non-INITIAL tickets, but kx509 "
                                "service is configured to require INITIAL "
                                "tickets");
        goto out;
    }

    if (ret == 0)
	ret = krb5_unparse_name(r->context, cprincipal, &r->cname);

    /* Check that the service name is a valid kx509 service name */
    if (ret == 0)
        ret = krb5_ticket_get_server(r->context, ticket, &sprincipal);
    if (ret == 0)
        r->realm = krb5_principal_get_realm(r->context, sprincipal);
    if (ret == 0)
        ret = krb5_unparse_name(r->context, sprincipal, &r->sname);
    if (ret == 0)
        ret = kdc_kx509_verify_service_principal(r->context, r, sprincipal);
    if (ret) {
        ret = mk_error_response(r->context, r, 4, ret,
                                "kx509 client used incorrect service name");
        goto out;
    }

    /* Authenticate the rest of the request */
    ret = verify_req_hash(r->context, &r->req, r->key);
    if (ret) {
        ret = mk_error_response(r->context, r, 4, ret,
                                "Incorrect request HMAC on kx509 request");
        goto out;
    }

    if (r->req.pk_key.length == 0) {
        /*
         * The request is an authenticated kx509 service availability probe.
         *
         * mk_error_response() will check whether the service is enabled and
         * possibly change the error code and message.
         */
        is_probe = 1;
        kdc_audit_addkv((kdc_request_t)r, 0, "probe", "authenticated");
        ret = mk_error_response(r->context, r, 4, 0,
                                "kx509 authenticated probe request");
        goto out;
    }

    /* Extract and parse CSR or a DER-encoded RSA public key */
    ret = get_csr(r->context, r);
    if (ret) {
	const char *msg = krb5_get_error_message(r->context, ret);
        ret = mk_error_response(r->context, r, 3, ret,
                                "Failed to parse CSR: %s", msg);
	krb5_free_error_message(r->context, msg);
        goto out;
    }

    /* Authorize the request */
    ret = check_authz(r->context, r, cprincipal);
    if (ret) {
	const char *msg = krb5_get_error_message(r->context, ret);
        ret = mk_error_response(r->context, r, 3, ret,
                                "Rejected by policy: %s", msg);
	krb5_free_error_message(r->context, msg);
        goto out;
    }

    /* Issue the certificate */
    ALLOC(rep.hash);
    ALLOC(rep.certificate);
    if (rep.certificate == NULL || rep.hash == NULL) {
        ret = mk_error_response(r->context, r, 0, ENOMEM,
                                "Could allocate memory for response");
        goto out;
    }

    krb5_data_zero(rep.hash);
    krb5_data_zero(rep.certificate);
    krb5_ticket_get_times(r->context, ticket, &r->ticket_times);
    ret = kdc_issue_certificate(r->context, r->config->app, r->logf, r->csr,
                                cprincipal, &r->ticket_times, 0 /*req_life*/,
                                r->send_chain, &certs);
    if (ret) {
        int level = 1;
	const char *msg = krb5_get_error_message(r->context, ret);

        if (ret == KRB5KDC_ERR_POLICY)
            level = 4; /* _kdc_audit_trail() logs at level 3 */
        ret = mk_error_response(r->context, r, level, ret,
                                "Certificate isuance failed: %s", msg);
	krb5_free_error_message(r->context, msg);
        goto out;
    }

    ret = encode_cert_and_chain(r->context->hx509ctx, certs, rep.certificate);
    if (ret) {
	const char *msg = krb5_get_error_message(r->context, ret);
        ret = mk_error_response(r->context, r, 1, ret,
                                "Could not encode certificate and chain: %s",
				msg);
	krb5_free_error_message(r->context, msg);
        goto out;
    }

    /* Authenticate the response */
    ret = calculate_reply_hash(r->context, r->key, &rep);
    if (ret) {
        ret = mk_error_response(r->context, r, 1, ret,
                                "Failed to compute response HMAC");
        goto out;
    }

    /* Encode and output reply */
    ret = encode_reply(r->context, r, &rep);
    if (ret)
        /* Can't send an error message either in this case, surely */
        kdc_audit_addreason((kdc_request_t)r, "Could not encode response");

out:
    hx509_certs_free(&certs);
    if (ret == 0 && !is_probe)
        kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", TRUE);
    else
        kdc_audit_setkv_bool((kdc_request_t)r, "cert_issued", FALSE);
    if (r->ac)
        krb5_auth_con_free(r->context, r->ac);
    if (ticket)
        krb5_free_ticket(r->context, ticket);
    if (id)
        krb5_kt_close(r->context, id);
    if (sprincipal)
        krb5_free_principal(r->context, sprincipal);
    if (cprincipal)
        krb5_free_principal(r->context, cprincipal);
    if (r->key)
        krb5_free_keyblock (r->context, r->key);
    hx509_request_free(&r->csr);
    free_Kx509CSRPlus(&r->csr_plus);
    free_Kx509Response(&rep);
    free_Kx509Request(&r->req);

    return ret;
}

#endif /* KX509 */
