/*
 * Copyright (c) 2019-2025 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.
 */

/*
 * Token validators for Bearer (JWT) and Negotiate (GSSAPI/Kerberos) tokens.
 *
 * This replaces the plugin-based token validation with inline validators
 * using OpenSSL for JWT and Heimdal's GSS-API for Negotiate tokens.
 */

#include "kdc_locl.h"
#include <base64.h>
#include <gssapi/gssapi.h>
#include "jwt_validator.h"

/*
 * Get configuration value from [bx509] realms section.
 */
static const char *
get_kv(krb5_context context, const char *realm, const char *k, const char *k2)
{
    return krb5_config_get_string(context, NULL, "bx509", "realms", realm,
                                  k, k2, NULL);
}

/*
 * Collect JWK paths from configuration for key rotation support.
 * Returns the number of paths found (0-3).
 */
static size_t
get_jwk_paths(krb5_context context,
              const char *realm,
              const char *paths[3])
{
    size_t n = 0;

    paths[0] = paths[1] = paths[2] = NULL;

    /*
     * We used to use libcjwt.  No more.  We'll keep the old config names for
     * now, but not document them because though we never shipped, these have
     * been in production.
     */

    /* Current key is tried first */
    if ((paths[n] = get_kv(context, realm, "jwk_current", NULL)) != NULL ||
        (paths[n] = get_kv(context, realm, "cjwt_jwk_current", NULL)) != NULL)
        n++;
    /* Then next key (for key rotation) */
    if ((paths[n] = get_kv(context, realm, "jwk_next", NULL)) != NULL ||
        (paths[n] = get_kv(context, realm, "cjwt_jwk_next", NULL)) != NULL)
        n++;
    /* Then previous key (for key rotation) */
    if ((paths[n] = get_kv(context, realm, "jwk_previous", NULL)) != NULL ||
        (paths[n] = get_kv(context, realm, "cjwt_jwk_previous", NULL)) != NULL)
        n++;

    return n;
}

/*
 * Validate a JWT Bearer token.
 */
static krb5_error_code
validate_bearer(krb5_context context,
                const char *realm,
                krb5_data *token,
                const char * const *audiences,
                size_t naudiences,
                krb5_boolean *result,
                krb5_principal *actual_principal,
                krb5_times *token_times)
{
    const char *jwk_paths[3];
    size_t njwk_paths;
    char *defrealm = NULL;
    krb5_error_code ret;

    *result = FALSE;
    *actual_principal = NULL;

    if (realm == NULL) {
        ret = krb5_get_default_realm(context, &defrealm);
        if (ret) {
            krb5_set_error_message(context, ret,
                                   "Could not determine default realm for JWT validation");
            return ret;
        }
        realm = defrealm;
    }

    njwk_paths = get_jwk_paths(context, realm, jwk_paths);
    if (njwk_paths == 0) {
        free(defrealm);
        krb5_set_error_message(context, ENOENT,
                               "No JWK configured for realm %s in "
                               "[bx509]->realms->%s->jwk_{current,next,previous}",
                               realm, realm);
        return ENOENT;
    }

    ret = validate_jwt_token(context,
                             token->data, token->length,
                             jwk_paths, njwk_paths,
                             audiences, naudiences,
                             result, actual_principal,
                             token_times, realm);

    free(defrealm);
    return ret;
}

/*
 * Display GSS-API status for error reporting.
 */
static krb5_error_code
gss_error(krb5_context context,
          OM_uint32 major,
          OM_uint32 minor,
          gss_OID mech_type,
          const char *prefix)
{
    gss_buffer_desc buf = GSS_C_EMPTY_BUFFER;
    OM_uint32 dmaj, dmin;
    OM_uint32 more = 0;
    char *msg = NULL;
    char *s = NULL;

    do {
        gss_release_buffer(&dmin, &buf);
        dmaj = gss_display_status(&dmin, major, GSS_C_GSS_CODE, GSS_C_NO_OID,
                                  &more, &buf);
        if (GSS_ERROR(dmaj) || buf.length == 0)
            break;
        if (asprintf(&s, "%s%s%.*s", msg ? msg : "", msg ? ": " : "",
                     (int)buf.length, (char *)buf.value) == -1) {
            free(msg);
            msg = NULL;
            break;
        }
        free(msg);
        msg = s;
        s = NULL;
    } while (!GSS_ERROR(dmaj) && more);

    if (mech_type != GSS_C_NO_OID && minor != 0) {
        more = 0;
        do {
            gss_release_buffer(&dmin, &buf);
            dmaj = gss_display_status(&dmin, minor, GSS_C_MECH_CODE, mech_type,
                                      &more, &buf);
            if (GSS_ERROR(dmaj) || buf.length == 0)
                break;
            if (asprintf(&s, "%s%s%.*s", msg ? msg : "", msg ? " (" : "",
                         (int)buf.length, (char *)buf.value) == -1) {
                break;
            }
            free(msg);
            msg = s;
            s = NULL;
            if (more == 0 && msg) {
                if (asprintf(&s, "%s)", msg) != -1) {
                    free(msg);
                    msg = s;
                }
            }
        } while (!GSS_ERROR(dmaj) && more);
    }
    gss_release_buffer(&dmin, &buf);

    if (msg)
        krb5_set_error_message(context, EACCES, "%s: %s", prefix, msg);
    else
        krb5_set_error_message(context, EACCES, "%s", prefix);
    free(msg);
    return EACCES;
}

/*
 * Validate a Negotiate (GSSAPI/Kerberos) token.
 */
static krb5_error_code
validate_negotiate(krb5_context context,
                   const char *realm,
                   krb5_data *token,
                   const char * const *audiences,
                   size_t naudiences,
                   krb5_boolean *result,
                   krb5_principal *actual_principal,
                   krb5_times *token_times)
{
    gss_buffer_desc adisplay_name = GSS_C_EMPTY_BUFFER;
    gss_buffer_desc idisplay_name = GSS_C_EMPTY_BUFFER;
    gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER;
    gss_buffer_desc input_token;
    gss_cred_id_t acred = GSS_C_NO_CREDENTIAL;
    gss_ctx_id_t gctx = GSS_C_NO_CONTEXT;
    gss_name_t aname = GSS_C_NO_NAME;
    gss_name_t iname = GSS_C_NO_NAME;
    gss_OID mech_type = GSS_C_NO_OID;
    const char *kt;
    OM_uint32 major, minor, ret_flags, time_rec;
    size_t i;
    char *token_decoded = NULL;
    void *token_copy = NULL;
    char *princ_str = NULL;
    krb5_error_code ret = 0;
    int decoded_len;

    *result = FALSE;
    *actual_principal = NULL;

    /* Get keytab from configuration */
    kt = krb5_config_get_string(context, NULL, "kdc",
                                "negotiate_token_validator", "keytab", NULL);
    if (kt) {
        gss_key_value_element_desc store_keytab_kv;
        gss_key_value_set_desc store;
        gss_OID_desc mech_set[2] = { *GSS_KRB5_MECHANISM, *GSS_SPNEGO_MECHANISM };
        gss_OID_set_desc mechs = { 2, mech_set };

        store_keytab_kv.key = "keytab";
        store_keytab_kv.value = kt;
        store.elements = &store_keytab_kv;
        store.count = 1;
        major = gss_acquire_cred_from(&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE,
                                      &mechs, GSS_C_ACCEPT, &store, &acred,
                                      NULL, NULL);
        if (major != GSS_S_COMPLETE) {
            ret = gss_error(context, major, minor, GSS_C_NO_OID,
                            "Failed to acquire GSS-API acceptor credential");
            goto out;
        }

        /* Restrict SPNEGO to Kerberos 5 only */
        mechs.count = 1;
        major = gss_set_neg_mechs(&minor, acred, &mechs);
        if (major != GSS_S_COMPLETE) {
            ret = gss_error(context, major, minor, GSS_C_NO_OID,
                            "Failed to set SPNEGO negotiation mechanisms");
            goto out;
        }
    } /* else use default credential */

    /* Base64 decode the token */
    token_decoded = malloc(token->length);
    token_copy = calloc(1, token->length + 1);
    if (token_decoded == NULL || token_copy == NULL) {
        ret = krb5_enomem(context);
        goto out;
    }

    memcpy(token_copy, token->data, token->length);
    decoded_len = rk_base64_decode(token_copy, token_decoded);
    if (decoded_len <= 0) {
        krb5_set_error_message(context, EACCES, "Negotiate token malformed");
        ret = EACCES;
        goto out;
    }

    /* Accept security context */
    input_token.value = token_decoded;
    input_token.length = decoded_len;
    major = gss_accept_sec_context(&minor, &gctx, acred, &input_token, NULL,
                                   &iname, &mech_type, &output_token,
                                   &ret_flags, &time_rec, NULL);

    /* Require Kerberos 5 mechanism */
    if (mech_type == GSS_C_NO_OID ||
        !gss_oid_equal(mech_type, GSS_KRB5_MECHANISM)) {
        krb5_set_error_message(context, EACCES,
                               "Negotiate token used non-Kerberos mechanism");
        ret = EACCES;
        goto out;
    }

    if (major != GSS_S_COMPLETE) {
        ret = gss_error(context, major, minor, mech_type,
                        "Failed to accept Negotiate token");
        goto out;
    }

    /* Get acceptor and initiator names */
    major = gss_inquire_context(&minor, gctx, NULL, &aname, NULL, NULL,
                                NULL, NULL, NULL);
    if (major == GSS_S_COMPLETE)
        major = gss_display_name(&minor, aname, &adisplay_name, NULL);
    if (major == GSS_S_COMPLETE)
        major = gss_display_name(&minor, iname, &idisplay_name, NULL);
    if (major != GSS_S_COMPLETE) {
        ret = gss_error(context, major, minor, mech_type,
                        "Failed to get names from GSS-API context");
        goto out;
    }

    /* Check audience (acceptor name must be HTTP/<audience>@REALM) */
    for (i = 0; i < naudiences; i++) {
        const char *s = adisplay_name.value;
        size_t slen = adisplay_name.length;
        size_t len = strlen(audiences[i]);

        if (slen >= sizeof("HTTP/") - 1       &&
            slen >= sizeof("HTTP/") - 1 + len &&
            memcmp(s, "HTTP/", sizeof("HTTP/") - 1) == 0 &&
            memcmp(s + sizeof("HTTP/") - 1, audiences[i], len) == 0 &&
            s[sizeof("HTTP/") - 1 + len] == '@')
            break;
    }
    if (i == naudiences) {
        krb5_set_error_message(context, EACCES,
                               "Negotiate token used wrong HTTP service "
                               "host acceptor name");
        ret = EACCES;
        goto out;
    }

    /* Parse initiator principal */
    princ_str = calloc(1, idisplay_name.length + 1);
    if (princ_str == NULL) {
        ret = krb5_enomem(context);
        goto out;
    }
    memcpy(princ_str, idisplay_name.value, idisplay_name.length);
    ret = krb5_parse_name(context, princ_str, actual_principal);
    if (ret)
        goto out;

    /* Set times (approximate since we don't have exact values) */
    token_times->authtime = 0;
    token_times->starttime = time(NULL) - 300;
    token_times->endtime = token_times->starttime + 300 + time_rec;
    token_times->renew_till = 0;

    *result = TRUE;

out:
    gss_delete_sec_context(&minor, &gctx, NULL);
    gss_release_buffer(&minor, &adisplay_name);
    gss_release_buffer(&minor, &idisplay_name);
    gss_release_buffer(&minor, &output_token);
    gss_release_cred(&minor, &acred);
    gss_release_name(&minor, &aname);
    gss_release_name(&minor, &iname);
    free(token_decoded);
    free(token_copy);
    free(princ_str);
    return ret;
}

/*
 * Validate a JWT/Bearer or Negotiate token.
 */
KDC_LIB_FUNCTION krb5_error_code KDC_LIB_CALL
kdc_validate_token(krb5_context context,
                   const char *realm,
                   const char *token_kind,
                   krb5_data *token,
                   const char * const *audiences,
                   size_t naudiences,
                   krb5_principal *actual_principal,
                   krb5_times *token_times)
{
    krb5_error_code ret;
    krb5_boolean result = FALSE;
    krb5_times times;

    memset(&times, 0, sizeof(times));
    if (actual_principal)
        *actual_principal = NULL;

    krb5_clear_error_message(context);

    if (strcasecmp(token_kind, "Bearer") == 0) {
        ret = validate_bearer(context, realm, token, audiences, naudiences,
                              &result, actual_principal, &times);
    } else if (strcasecmp(token_kind, "Negotiate") == 0) {
        ret = validate_negotiate(context, realm, token, audiences, naudiences,
                                 &result, actual_principal, &times);
    } else {
        krb5_set_error_message(context, EINVAL,
                               "Unknown token type '%s' (expected Bearer or Negotiate)",
                               token_kind);
        return EINVAL;
    }

    if (token_times)
        *token_times = times;

    if (ret) {
        krb5_prepend_error_message(context, ret, "token validation failed: ");
        if (actual_principal) {
            krb5_free_principal(context, *actual_principal);
            *actual_principal = NULL;
        }
    } else if (!result) {
        krb5_set_error_message(context, EACCES, "token validation failed");
        ret = EACCES;
        if (actual_principal) {
            krb5_free_principal(context, *actual_principal);
            *actual_principal = NULL;
        }
    }

    return ret;
}
