/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* plugins/kdb/hdb/kdb_windc.c */
/*
 * Copyright 2009 by the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * Export of this software from the United States of America may
 *   require a specific license from the United States Government.
 *   It is the responsibility of any person or organization contemplating
 *   export to obtain such a license before exporting.
 *
 * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
 * distribute this software and its documentation for any purpose and
 * without fee is hereby granted, provided that the above copyright
 * notice appear in all copies and that both that copyright notice and
 * this permission notice appear in supporting documentation, and that
 * the name of M.I.T. not be used in advertising or publicity pertaining
 * to distribution of the software without specific, written prior
 * permission.  Furthermore if you modify this software you must label
 * your software as modified software and not distribute it in such a
 * fashion that it might be confused with the original M.I.T. software.
 * M.I.T. makes no representations about the suitability of
 * this software for any purpose.  It is provided "as is" without express
 * or implied warranty.
 */

#include "k5-int.h"

#if HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <db.h>
#include <stdio.h>
#include <errno.h>
#include <utime.h>
#include "kdb5.h"
#include "kdb_hdb.h"

/*
 * WinDC helpers
 */

static krb5_error_code
kh_windc_pac_generate(krb5_context context,
                      kh_db_context *kh,
                      struct hdb_entry_ex *hentry,
                      heim_pac *pac)
{
    if (kh->windc == NULL || kh->windc->pac_generate == NULL)
        return KRB5_KDB_DBTYPE_NOSUP;

    return kh_map_error((*kh->windc->pac_generate)(kh->windc_ctx,
                                                   kh->hcontext,
                                                   hentry,
                                                   pac));
}

static krb5_error_code
kh_windc_pac_verify(krb5_context context,
                    kh_db_context *kh,
                    const Principal *principal,
                    struct hdb_entry_ex *client,
                    struct hdb_entry_ex *server,
                    heim_pac *pac)
{
    if (kh->windc == NULL || kh->windc->pac_verify == NULL)
        return KRB5_KDB_DBTYPE_NOSUP;

    return kh_map_error((*kh->windc->pac_verify)(kh->windc_ctx,
                                                 kh->hcontext,
                                                 principal,
                                                 client,
                                                 server,
                                                 pac));
}

static krb5_error_code
kh_windc_client_access(krb5_context context,
                       kh_db_context *kh,
                       struct hdb_entry_ex *client,
                       KDC_REQ *req,
                       heim_octet_string *e_data)
{
    if (kh->windc == NULL || kh->windc->client_access == NULL)
        return KRB5_KDB_DBTYPE_NOSUP;

    return kh_map_error((*kh->windc->client_access)(kh->windc_ctx,
                                                    kh->hcontext,
                                                    client,
                                                    req,
                                                    e_data));
}

static void
kh_pac_free(krb5_context context, heim_pac pac)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);

    assert(kh->heim_pac_free != NULL);

    if (pac != NULL)
        (*kh->heim_pac_free)(kh->hcontext, pac);
}

static krb5_error_code
kh_pac_parse(krb5_context context,
             const void *data,
             size_t len,
             heim_pac *pac)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);

    return kh_map_error((*kh->heim_pac_parse)(kh->hcontext,
                                              data,
                                              len,
                                              pac));
}

static krb5_error_code
kh_pac_verify(krb5_context context,
              const heim_pac pac,
              time_t authtime,
              const Principal *princ,
              const EncryptionKey *server,
              const EncryptionKey *krbtgt)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);

    assert(kh->heim_pac_verify != NULL);

    return kh_map_error((*kh->heim_pac_verify)(kh->hcontext,
                                               pac,
                                               authtime,
                                               princ,
                                               server,
                                               krbtgt));
}

static krb5_error_code
kh_pac_sign(krb5_context context,
            heim_pac pac,
            time_t authtime,
            Principal *princ,
            const EncryptionKey *server,
            const EncryptionKey *krbtgt,
            heim_octet_string *data)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);

    assert(kh->heim_pac_sign != NULL);

    return kh_map_error((*kh->heim_pac_sign)(kh->hcontext,
                                             pac,
                                             authtime,
                                             princ,
                                             server,
                                             krbtgt,
                                             data));
}

/*
 * Get local TGS key for the realm of the supplied principal.
 */
static krb5_error_code
kh_get_tgs_key(krb5_context context,
               kh_db_context *kh,
               const krb5_principal princ,
               krb5_keyblock *krbtgt_keyblock)
{
    krb5_error_code code;
    krb5_principal tgsname = NULL;
    krb5_key_data *krbtgt_key = NULL;
    krb5_db_entry *krbtgt;

    memset(&krbtgt, 0, sizeof(krbtgt));
    krbtgt_keyblock->contents = NULL;

    code = krb5_build_principal_ext(context,
                                    &tgsname,
                                    princ->realm.length,
                                    princ->realm.data,
                                    KRB5_TGS_NAME_SIZE,
                                    KRB5_TGS_NAME,
                                    princ->realm.length,
                                    princ->realm.data,
                                    0);
    if (code != 0)
        goto cleanup;

    code = kh_get_principal(context, kh, tgsname, HDB_F_GET_KRBTGT, &krbtgt);
    if (code != 0)
        goto cleanup;

    code = krb5_dbe_find_enctype(context,
                                 krbtgt,
                                 -1,    /* ignore enctype */
                                 -1,    /* ignore salttype */
                                 0,     /* highest kvno */
                                 &krbtgt_key);
    if (code != 0)
        goto cleanup;
    else if (krbtgt_key == NULL) {
        code = KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN;
        goto cleanup;
    }

    code = kh_decrypt_key(context,
                          KH_DB_CONTEXT(context),
                          krbtgt_key,
                          krbtgt_keyblock,
                          NULL);
    if (code != 0)
        goto cleanup;

cleanup:
    kh_kdb_free_entry(context, KH_DB_CONTEXT(context), krbtgt);
    krb5_free_principal(context, tgsname);

    return code;
}

krb5_error_code
kh_db_sign_auth_data(krb5_context context,
                     unsigned int flags,
                     krb5_const_principal client_princ,
                     krb5_db_entry *client,
                     krb5_db_entry *server,
                     krb5_db_entry *krbtgt,
                     krb5_keyblock *client_key,
                     krb5_keyblock *server_key,
                     krb5_keyblock *krbtgt_key,
                     krb5_keyblock *session_key,
                     krb5_timestamp authtime,
                     krb5_authdata **tgt_auth_data,
                     krb5_authdata ***signed_auth_data)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);
    heim_pac hpac = NULL;
    heim_octet_string pac_data;
    krb5_boolean is_as_req;
    krb5_error_code code;
    krb5_authdata **authdata = NULL;
    Principal *client_hprinc = NULL;
    EncryptionKey server_hkey;
    EncryptionKey krbtgt_hkey;
    krb5_keyblock krbtgt_kkey;

    *signed_auth_data = NULL;

    if (kh->windc == NULL)
        return KRB5_KDB_DBTYPE_NOSUP; /* short circuit */

    memset(&krbtgt_kkey, 0, sizeof(krbtgt_kkey));
    pac_data.data = NULL;

    is_as_req = ((flags & KRB5_KDB_FLAG_CLIENT_REFERRALS_ONLY) != 0);

    /* Prefer canonicalised name from client entry */
    if (client != NULL) {
        client_hprinc = KH_DB_ENTRY(client)->entry.principal;
    } else {
        code = kh_marshal_Principal(context, client_princ, &client_hprinc);
        if (code != 0)
            goto cleanup;
    }

    KH_MARSHAL_KEY(server_key, &server_hkey);
    KH_MARSHAL_KEY(krbtgt_key, &krbtgt_hkey);

    if (!is_as_req) {
        /* find the existing PAC, if present */
        code = krb5_find_authdata(context, tgt_auth_data, NULL,
                                  KRB5_AUTHDATA_WIN2K_PAC, &authdata);
        if (code != 0)
            goto cleanup;
    }

    if ((is_as_req && (flags & KRB5_KDB_FLAG_INCLUDE_PAC)) ||
        (authdata == NULL && client != NULL)) {
        code = kh_windc_pac_generate(context, kh, KH_DB_ENTRY(client), &hpac);
        if (code != 0)
            goto cleanup;
    } else if (authdata != NULL) {
        assert(authdata[0] != NULL);

        if (authdata[1] != NULL) {
            code = KRB5KDC_ERR_BADOPTION; /* XXX */
            goto cleanup;
        }

        pac_data.data   = authdata[0]->contents;
        pac_data.length = authdata[0]->length;

        code = kh_pac_parse(context, pac_data.data, pac_data.length, &hpac);
        if (code != 0)
            goto cleanup;

        /*
         * In the constrained delegation case, the PAC is from a service
         * ticket rather than a TGT; we must verify the server and KDC
         * signatures to assert that the server did not forge the PAC.
         */
        if (flags & KRB5_KDB_FLAG_CONSTRAINED_DELEGATION) {
            code = kh_pac_verify(context, hpac, authtime,
                                 client_hprinc, &server_hkey, &krbtgt_hkey);
        } else {
            code = kh_pac_verify(context, hpac, authtime,
                                 client_hprinc, &krbtgt_hkey, NULL);
        }
        if (code != 0)
            goto cleanup;

        code = kh_windc_pac_verify(context, kh, client_hprinc,
                                   client ? KH_DB_ENTRY(client) : NULL,
                                   KH_DB_ENTRY(server),
                                   &hpac);
        if (code != 0)
            goto cleanup;
    } else {
        code = KRB5_KDB_DBTYPE_NOSUP;
        goto cleanup;
    }

    /*
     * In the cross-realm case, krbtgt_hkey refers to the cross-realm
     * TGS key, so we need to explicitly lookup our TGS key.
     */
    if (flags & KRB5_KDB_FLAG_CROSS_REALM) {
        assert(!is_as_req);

        code = kh_get_tgs_key(context, kh, server->princ, &krbtgt_kkey);
        if (code != 0)
            goto cleanup;

        KH_MARSHAL_KEY(&krbtgt_kkey, &krbtgt_hkey);
    }

    code = kh_pac_sign(context, hpac, authtime, client_hprinc,
                       &server_hkey, &krbtgt_hkey, &pac_data);
    if (code != 0)
        goto cleanup;

    if (authdata == NULL) {
        authdata = k5calloc(2, sizeof(krb5_authdata *), &code);
        if (code != 0)
            goto cleanup;

        authdata[0] = k5alloc(sizeof(krb5_authdata), &code);
        if (code != 0)
            goto cleanup;

        authdata[1] = NULL;
    } else {
        free(authdata[0]->contents);
        authdata[0]->contents = NULL;
        authdata[0]->length = 0;
    }

    /* take ownership of pac_data */
    authdata[0]->magic    = KV5M_AUTHDATA;
    authdata[0]->ad_type  = KRB5_AUTHDATA_WIN2K_PAC;
    authdata[0]->contents = pac_data.data;
    authdata[0]->length   = pac_data.length;

    pac_data.data = NULL;

    code = krb5_encode_authdata_container(context,
                                          KRB5_AUTHDATA_IF_RELEVANT,
                                          authdata,
                                          signed_auth_data);
    if (code != 0)
        goto cleanup;

cleanup:
    if (client == NULL)
        kh_free_Principal(context, client_hprinc);
    kh_pac_free(context, hpac);
    if (pac_data.data != NULL)
        free(pac_data.data);
    krb5_free_authdata(context, authdata);
    krb5_free_keyblock_contents(context, &krbtgt_kkey);

    return code;
}

static krb5_error_code
kh_marshal_KDCOptions(krb5_context context,
                      krb5_flags koptions,
                      KDCOptions *hoptions)
{
    memset(hoptions, 0, sizeof(*hoptions));

    if (koptions & KDC_OPT_FORWARDABLE)
        hoptions->forwardable = 1;
    if (koptions & KDC_OPT_FORWARDED)
        hoptions->forwarded = 1;
    if (koptions & KDC_OPT_PROXIABLE)
        hoptions->proxiable = 1;
    if (koptions & KDC_OPT_PROXY)
        hoptions->proxy = 1;
    if (koptions & KDC_OPT_ALLOW_POSTDATE)
        hoptions->allow_postdate = 1;
    if (koptions & KDC_OPT_POSTDATED)
        hoptions->postdated = 1;
    if (koptions & KDC_OPT_RENEWABLE)
        hoptions->renewable = 1;
    if (koptions & KDC_OPT_REQUEST_ANONYMOUS)
        hoptions->request_anonymous = 1;
    if (koptions & KDC_OPT_CANONICALIZE)
        hoptions->canonicalize = 1;
    if (koptions & KDC_OPT_DISABLE_TRANSITED_CHECK)
        hoptions->disable_transited_check = 1;
    if (koptions & KDC_OPT_RENEWABLE_OK)
        hoptions->renewable_ok = 1;
    if (koptions & KDC_OPT_ENC_TKT_IN_SKEY)
        hoptions->enc_tkt_in_skey = 1;
    if (koptions & KDC_OPT_RENEW)
        hoptions->renew = 1;
    if (koptions & KDC_OPT_VALIDATE)
        hoptions->validate = 1;

    return 0;
}

static krb5_error_code
kh_marshall_HostAddress(krb5_context context,
                        krb5_address *kaddress,
                        HostAddress *haddress)
{
    haddress->addr_type = kaddress->addrtype;
    haddress->address.data = malloc(kaddress->length);
    if (haddress->address.data == NULL)
        return ENOMEM;

    memcpy(haddress->address.data, kaddress->contents, kaddress->length);
    haddress->address.length = kaddress->length;

    return 0;
}

static krb5_error_code
kh_marshall_HostAddresses(krb5_context context,
                          krb5_address **kaddresses,
                          HostAddresses **phaddresses)
{
    krb5_error_code code;
    HostAddresses *haddresses;
    int i;

    *phaddresses = NULL;

    if (kaddresses == NULL)
        return 0;

    for (i = 0; kaddresses[i] != NULL; i++)
        ;

    haddresses = k5alloc(sizeof(*haddresses), &code);
    if (code != 0)
        return code;

    haddresses->len = 0;
    haddresses->val = k5calloc(i, sizeof(HostAddress), &code);
    if (code != 0)
        return code;

    for (i = 0; kaddresses[i] != NULL; i++) {
        code = kh_marshall_HostAddress(context,
                                       kaddresses[i],
                                       &haddresses->val[i]);
        if (code != 0)
            break;

        haddresses->len++;
    }

    if (code != 0) {
        free(haddresses->val);
        free(haddresses);
    } else {
        *phaddresses = haddresses;
    }

    return code;
}

krb5_error_code
kh_db_check_policy_as(krb5_context context,
                      krb5_kdc_req *request,
                      krb5_db_entry *client,
                      krb5_db_entry *server,
                      krb5_timestamp kdc_time,
                      const char **status,
                      krb5_pa_data ***e_data_out)
{
    kh_db_context *kh = KH_DB_CONTEXT(context);
    krb5_error_code code;
    krb5_data d;
    krb5_pa_data **e_data;
    heim_octet_string he_data;
    KDC_REQ hkdcreq;
    Principal *hclient = NULL;
    Principal *hserver = NULL;
    time_t from, till, rtime;

    if (kh->windc == NULL)
        return KRB5_KDB_DBTYPE_NOSUP; /* short circuit */

    memset(&hkdcreq, 0, sizeof(hkdcreq));

    hkdcreq.pvno = KRB5_PVNO;
    hkdcreq.msg_type = request->msg_type;
    hkdcreq.padata = NULL; /* FIXME */
    code = kh_marshal_KDCOptions(context,
                                 request->kdc_options,
                                 &hkdcreq.req_body.kdc_options);
    if (code != 0)
        goto cleanup;

    code = kh_marshal_Principal(context, request->client, &hclient);
    if (code != 0)
        goto cleanup;

    code = kh_marshal_Principal(context, request->server, &hserver);
    if (code != 0)
        goto cleanup;

    hkdcreq.req_body.cname = &hclient->name;
    hkdcreq.req_body.realm = hserver->realm;
    hkdcreq.req_body.sname = &hserver->name;

    from  = request->from;  hkdcreq.req_body.from = &from;
    till  = request->till;  hkdcreq.req_body.till = &till;
    rtime = request->rtime; hkdcreq.req_body.rtime = &rtime;

    hkdcreq.req_body.nonce     = request->nonce;
    hkdcreq.req_body.etype.len = request->nktypes;
    hkdcreq.req_body.etype.val = request->ktype;

    code = kh_marshall_HostAddresses(context,
                                     request->addresses,
                                     &hkdcreq.req_body.addresses);
    if (code != 0)
        goto cleanup;

    /* FIXME hkdcreq.req_body.enc_authorization_data */
    /* FIXME hkdcreq.req_body.additional_tickets */

    code = kh_windc_client_access(context, kh,
                                  KH_DB_ENTRY(client),
                                  &hkdcreq, &he_data);

    if (he_data.data != NULL) {
        d = make_data(he_data.data, he_data.length);
        code = decode_krb5_padata_sequence(&d, &e_data);
        if (code == 0)
            *e_data_out = e_data;
        free(he_data.data);
        code = 0;
    }

cleanup:
    kh_free_HostAddresses(context, hkdcreq.req_body.addresses);
    kh_free_Principal(context, hclient);
    kh_free_Principal(context, hserver);

    return code;
}

krb5_error_code
kh_hdb_windc_init(krb5_context context,
                  const char *libdir,
                  kh_db_context *kh)
{
    krb5_error_code code;
    const char *objdirs[2];
    void **tables = NULL;
    int i;

    memset(&kh->windc_plugins, 0, sizeof(kh->windc_plugins));

    code = PLUGIN_DIR_OPEN(&kh->windc_plugins);
    if (code != 0)
        return code;

    objdirs[0] = libdir;
    objdirs[1] = NULL;

    code = krb5int_open_plugin_dirs(objdirs, NULL,
                                    &kh->windc_plugins,
                                    &context->err);
    if (code != 0)
        return code;

    code = krb5int_get_plugin_dir_data(&kh->windc_plugins,
                                       "windc",
                                       &tables,
                                       &context->err);
    if (code != 0)
        return code;

    code = KRB5_KDB_DBTYPE_NOSUP;

    for (i = 0; tables[i] != NULL; i++) {
        krb5plugin_windc_ftable *windc = tables[i];

        if (windc->minor_version < KRB5_WINDC_PLUGIN_MINOR)
            continue;

        code = kh_map_error((*windc->init)(kh->hcontext, &kh->windc_ctx));
        if (code != 0)
            continue;

        kh->windc = windc;
        break;
    }

    if (tables != NULL)
        krb5int_free_plugin_dir_data(tables);

    return code;
}
