436 lines
15 KiB
C
436 lines
15 KiB
C
/*
|
|
* 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(×, 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, ×);
|
|
} else if (strcasecmp(token_kind, "Negotiate") == 0) {
|
|
ret = validate_negotiate(context, realm, token, audiences, naudiences,
|
|
&result, actual_principal, ×);
|
|
} 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;
|
|
}
|