kdc: Replace token validator plugin system
This commit is contained in:
32
configure.ac
32
configure.ac
@@ -230,38 +230,6 @@ AM_CONDITIONAL([HAVE_MICROHTTPD], [test "$with_microhttpd" != "no"])
|
||||
AC_SUBST([MICROHTTPD_CFLAGS])
|
||||
AC_SUBST([MICROHTTPD_LIBS])
|
||||
|
||||
dnl libcjwt
|
||||
AC_ARG_WITH([cjwt],
|
||||
AC_HELP_STRING([--with-cjwt], [(Experimental) use cjwt to validate JWT tokens @<:@default=check@:>@]),
|
||||
[],
|
||||
[with_cjwt=check])
|
||||
if test "$with_cjwt" != "no"; then
|
||||
PKG_CHECK_MODULES([CJWT], [libcjwt >= 1.0.0],
|
||||
[with_cjwt=yes],[with_cjwt=no])
|
||||
fi
|
||||
if test "$with_cjwt" = "yes"; then
|
||||
AC_DEFINE_UNQUOTED([HAVE_CJWT], 1, [whether libcjwt is available for KDC REST API])
|
||||
fi
|
||||
AM_CONDITIONAL([HAVE_CJWT], [test "$with_cjwt" != "no"])
|
||||
AC_SUBST([CJWT_CFLAGS])
|
||||
AC_SUBST([CJWT_LIBS])
|
||||
|
||||
dnl libcjson
|
||||
AC_ARG_WITH([cjson],
|
||||
AC_HELP_STRING([--with-cjson], [(Experimental) use cJSON to extract private claims from JWT tokens @<:@default=check@:>@]),
|
||||
[],
|
||||
[with_cjson=check])
|
||||
if test "$with_cjson" != "no"; then
|
||||
PKG_CHECK_MODULES([CJSON], [libcjson >= 1.0.0],
|
||||
[with_cjson=yes],[with_cjson=no])
|
||||
fi
|
||||
if test "$with_cjson" = "yes"; then
|
||||
AC_DEFINE_UNQUOTED([HAVE_CJSON], 1, [whether libcjson is available for KDC REST API])
|
||||
fi
|
||||
AM_CONDITIONAL([HAVE_CJSON], [test "$with_cjson" != "no"])
|
||||
AC_SUBST([CJSON_CFLAGS])
|
||||
AC_SUBST([CJSON_LIBS])
|
||||
|
||||
dnl mitkrb5
|
||||
AC_ARG_WITH([mitkrb5],
|
||||
AC_HELP_STRING([--with-mitkrb5], [Path to MIT Kerberos for interop testing @<:@default=check@:>@]),
|
||||
|
||||
@@ -133,7 +133,6 @@ CLEANFILES = \
|
||||
kdc-audit.h \
|
||||
csr_authorizer_plugin.h \
|
||||
gss_preauth_authorizer_plugin.h \
|
||||
token_validator_plugin.h \
|
||||
xdbm.h \
|
||||
x690sample_asn1.h \
|
||||
x690sample_template_asn1.h
|
||||
|
||||
@@ -7,12 +7,7 @@ WFLAGS += $(WFLAGS_ENUM_CONV)
|
||||
AM_CPPFLAGS += $(INCLUDE_libintl) $(INCLUDE_openssl_crypto) -I$(srcdir)/../lib/krb5
|
||||
|
||||
lib_LTLIBRARIES = ipc_csr_authorizer.la \
|
||||
negotiate_token_validator.la \
|
||||
libkdc.la
|
||||
|
||||
if HAVE_CJWT
|
||||
lib_LTLIBRARIES += cjwt_token_validator.la
|
||||
endif
|
||||
if OPENLDAP
|
||||
lib_LTLIBRARIES += altsecid_gss_preauth_authorizer.la
|
||||
endif
|
||||
@@ -23,7 +18,8 @@ bin_PROGRAMS = string2key
|
||||
sbin_PROGRAMS = kstash
|
||||
|
||||
libexec_PROGRAMS = hprop hpropd kdc \
|
||||
test_token_validator test_csr_authorizer test_kdc_ca
|
||||
test_token_validator test_csr_authorizer test_kdc_ca \
|
||||
test_jwt_validator
|
||||
|
||||
noinst_PROGRAMS = kdc-replay kdc-tester
|
||||
|
||||
@@ -84,16 +80,8 @@ kdc_tester_SOURCES = \
|
||||
test_token_validator_SOURCES = test_token_validator.c
|
||||
test_csr_authorizer_SOURCES = test_csr_authorizer.c
|
||||
test_kdc_ca_SOURCES = test_kdc_ca.c
|
||||
test_jwt_validator_SOURCES = test_jwt_validator.c jwt_validator.c
|
||||
|
||||
# Token plugins (for bx509d)
|
||||
if HAVE_CJWT
|
||||
cjwt_token_validator_la_SOURCES = cjwt_token_validator.c
|
||||
cjwt_token_validator_la_CFLAGS = $(CJSON_CFLAGS) $(CJWT_CFLAGS)
|
||||
cjwt_token_validator_la_LDFLAGS = -module $(CJSON_LIBS) $(CJWT_LIBS)
|
||||
endif
|
||||
|
||||
negotiate_token_validator_la_SOURCES = negotiate_token_validator.c
|
||||
negotiate_token_validator_la_LDFLAGS = -module $(LIB_gssapi)
|
||||
# CSR Authorizer plugins (for kdc/kx509 and bx509d)
|
||||
ipc_csr_authorizer_la_SOURCES = ipc_csr_authorizer.c
|
||||
ipc_csr_authorizer_la_LDFLAGS = -module \
|
||||
@@ -127,6 +115,8 @@ libkdc_la_SOURCES = \
|
||||
misc.c \
|
||||
kx509.c \
|
||||
token_validator.c \
|
||||
jwt_validator.c \
|
||||
jwt_validator.h \
|
||||
csr_authorizer.c \
|
||||
process.c \
|
||||
kdc-plugin.c \
|
||||
@@ -147,12 +137,11 @@ ALL_OBJECTS += $(hprop_OBJECTS)
|
||||
ALL_OBJECTS += $(hpropd_OBJECTS)
|
||||
ALL_OBJECTS += $(bx509d_OBJECTS)
|
||||
ALL_OBJECTS += $(httpkadmind_OBJECTS)
|
||||
ALL_OBJECTS += $(cjwt_token_validator_la_OBJECTS)
|
||||
ALL_OBJECTS += $(test_token_validator_OBJECTS)
|
||||
ALL_OBJECTS += $(test_csr_authorizer_OBJECTS)
|
||||
ALL_OBJECTS += $(test_kdc_ca_OBJECTS)
|
||||
ALL_OBJECTS += $(test_jwt_validator_OBJECTS)
|
||||
ALL_OBJECTS += $(ipc_csr_authorizer_la_OBJECTS)
|
||||
ALL_OBJECTS += $(negotiate_token_validator_la_OBJECTS)
|
||||
|
||||
$(ALL_OBJECTS): $(KDC_PROTOS)
|
||||
|
||||
@@ -226,13 +215,14 @@ test_csr_authorizer_LDADD = libkdc.la \
|
||||
$(LIB_heimbase) \
|
||||
$(top_builddir)/lib/ipc/libheim-ipcs.la
|
||||
test_kdc_ca_LDADD = libkdc.la $(top_builddir)/lib/hx509/libhx509.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase)
|
||||
test_jwt_validator_LDADD = libkdc.la $(top_builddir)/lib/hx509/libhx509.la $(LDADD) $(LIB_pidfile) $(LIB_heimbase)
|
||||
|
||||
include_HEADERS = kdc.h $(srcdir)/kdc-protos.h
|
||||
|
||||
noinst_HEADERS = $(srcdir)/kdc-private.h
|
||||
|
||||
krb5dir = $(includedir)/krb5
|
||||
krb5_HEADERS = kdc-audit.h kdc-plugin.h kdc-accessors.h token_validator_plugin.h csr_authorizer_plugin.h gss_preauth_authorizer_plugin.h
|
||||
krb5_HEADERS = kdc-audit.h kdc-plugin.h kdc-accessors.h csr_authorizer_plugin.h gss_preauth_authorizer_plugin.h
|
||||
|
||||
build_HEADERZ = $(krb5_HEADERS) # XXX
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ LIBKDC_OBJS=\
|
||||
$(OBJ)\misc.obj \
|
||||
$(OBJ)\kx509.obj \
|
||||
$(OBJ)\token_validator.obj \
|
||||
$(OBJ)\jwt_validator.obj \
|
||||
$(OBJ)\csr_authorizer.obj \
|
||||
$(OBJ)\process.obj \
|
||||
$(OBJ)\kdc-plugin.obj \
|
||||
@@ -142,6 +143,7 @@ libkdc_la_SOURCES = \
|
||||
misc.c \
|
||||
kx509.c \
|
||||
token_validator.c \
|
||||
jwt_validator.c \
|
||||
csr_authorizer.c \
|
||||
process.c \
|
||||
kdc-plugin.c \
|
||||
|
||||
@@ -128,7 +128,6 @@
|
||||
|
||||
#include <microhttpd.h>
|
||||
#include "kdc_locl.h"
|
||||
#include "token_validator_plugin.h"
|
||||
#include <getarg.h>
|
||||
#include <roken.h>
|
||||
#include <krb5.h>
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This is a plugin by which bx509d can validate JWT Bearer tokens using the
|
||||
* cjwt library.
|
||||
*
|
||||
* Configuration:
|
||||
*
|
||||
* [kdc]
|
||||
* realm = {
|
||||
* A.REALM.NAME = {
|
||||
* cjwt_jqk = PATH-TO-JWK-PEM-FILE
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* where AUDIENCE-FOR-KDC is the value of the "audience" (i.e., the target) of
|
||||
* the token.
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <string.h>
|
||||
#include <heimbase.h>
|
||||
#include <krb5.h>
|
||||
#include <common_plugin.h>
|
||||
#include <hdb.h>
|
||||
#include <roken.h>
|
||||
#include <token_validator_plugin.h>
|
||||
#include <cjwt/cjwt.h>
|
||||
#ifdef HAVE_CJSON
|
||||
#include <cJSON.h>
|
||||
#endif
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static krb5_error_code
|
||||
get_issuer_pubkeys(krb5_context context,
|
||||
const char *realm,
|
||||
krb5_data *previous,
|
||||
krb5_data *current,
|
||||
krb5_data *next)
|
||||
{
|
||||
krb5_error_code save_ret = 0;
|
||||
krb5_error_code ret;
|
||||
const char *v;
|
||||
size_t nkeys = 0;
|
||||
|
||||
previous->data = current->data = next->data = 0;
|
||||
previous->length = current->length = next->length = 0;
|
||||
|
||||
if ((v = get_kv(context, realm, "cjwt_jwk_next", NULL)) &&
|
||||
(++nkeys) &&
|
||||
(ret = rk_undumpdata(v, &next->data, &next->length)))
|
||||
save_ret = ret;
|
||||
if ((v = get_kv(context, realm, "cjwt_jwk_previous", NULL)) &&
|
||||
(++nkeys) &&
|
||||
(ret = rk_undumpdata(v, &previous->data, &previous->length)) &&
|
||||
save_ret == 0)
|
||||
save_ret = ret;
|
||||
if ((v = get_kv(context, realm, "cjwt_jwk_current", NULL)) &&
|
||||
(++nkeys) &&
|
||||
(ret = rk_undumpdata(v, ¤t->data, ¤t->length)) &&
|
||||
save_ret == 0)
|
||||
save_ret = ret;
|
||||
if (nkeys == 0)
|
||||
krb5_set_error_message(context, EINVAL, "jwk issuer key not specified in "
|
||||
"[bx509]->realm->%s->cjwt_jwk_{previous,current,next}",
|
||||
realm);
|
||||
if (!previous->length && !current->length && !next->length)
|
||||
krb5_set_error_message(context, save_ret,
|
||||
"Could not read jwk issuer public key files");
|
||||
if (current->length && current->length == next->length &&
|
||||
memcmp(current->data, next->data, next->length) == 0) {
|
||||
free(next->data);
|
||||
next->data = 0;
|
||||
next->length = 0;
|
||||
}
|
||||
if (current->length && current->length == previous->length &&
|
||||
memcmp(current->data, previous->data, previous->length) == 0) {
|
||||
free(previous->data);
|
||||
previous->data = 0;
|
||||
previous->length = 0;
|
||||
}
|
||||
|
||||
if (previous->data == NULL && current->data == NULL && next->data == NULL)
|
||||
return krb5_set_error_message(context, ENOENT, "No JWKs found"),
|
||||
ENOENT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static krb5_error_code
|
||||
check_audience(krb5_context context,
|
||||
const char *realm,
|
||||
cjwt_t *jwt,
|
||||
const char * const *audiences,
|
||||
size_t naudiences)
|
||||
{
|
||||
size_t i, k;
|
||||
|
||||
if (!jwt->aud) {
|
||||
krb5_set_error_message(context, EACCES, "JWT bearer token has no "
|
||||
"audience");
|
||||
return EACCES;
|
||||
}
|
||||
for (i = 0; i < jwt->aud->count; i++)
|
||||
for (k = 0; k < naudiences; k++)
|
||||
if (strcasecmp(audiences[k], jwt->aud->names[i]) == 0)
|
||||
return 0;
|
||||
krb5_set_error_message(context, EACCES, "JWT bearer token's audience "
|
||||
"does not match any expected audience");
|
||||
return EACCES;
|
||||
}
|
||||
|
||||
static krb5_error_code
|
||||
get_princ(krb5_context context,
|
||||
const char *realm,
|
||||
cjwt_t *jwt,
|
||||
krb5_principal *actual_principal)
|
||||
{
|
||||
krb5_error_code ret;
|
||||
const char *force_realm = NULL;
|
||||
const char *domain;
|
||||
|
||||
#ifdef HAVE_CJSON
|
||||
if (jwt->private_claims) {
|
||||
cJSON *jval;
|
||||
|
||||
if ((jval = cJSON_GetObjectItem(jwt->private_claims, "authz_sub")))
|
||||
return krb5_parse_name(context, jval->valuestring, actual_principal);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (jwt->sub == NULL) {
|
||||
krb5_set_error_message(context, EACCES, "JWT token lacks 'sub' "
|
||||
"(subject name)!");
|
||||
return EACCES;
|
||||
}
|
||||
if ((domain = strchr(jwt->sub, '@'))) {
|
||||
force_realm = get_kv(context, realm, "cjwt_force_realm", ++domain);
|
||||
ret = krb5_parse_name(context, jwt->sub, actual_principal);
|
||||
} else {
|
||||
ret = krb5_parse_name_flags(context, jwt->sub,
|
||||
KRB5_PRINCIPAL_PARSE_NO_REALM,
|
||||
actual_principal);
|
||||
}
|
||||
if (ret)
|
||||
krb5_set_error_message(context, ret, "JWT token 'sub' not a valid "
|
||||
"principal name: %s", jwt->sub);
|
||||
else if (force_realm)
|
||||
ret = krb5_principal_set_realm(context, *actual_principal, realm);
|
||||
else if (domain == NULL)
|
||||
ret = krb5_principal_set_realm(context, *actual_principal, realm);
|
||||
/* else leave the domain as the realm */
|
||||
return ret;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL krb5_error_code
|
||||
validate(void *ctx,
|
||||
krb5_context context,
|
||||
const char *realm,
|
||||
const char *token_type,
|
||||
krb5_data *token,
|
||||
const char * const *audiences,
|
||||
size_t naudiences,
|
||||
krb5_boolean *result,
|
||||
krb5_principal *actual_principal,
|
||||
krb5_times *token_times)
|
||||
{
|
||||
heim_octet_string jwk_previous;
|
||||
heim_octet_string jwk_current;
|
||||
heim_octet_string jwk_next;
|
||||
cjwt_t *jwt = NULL;
|
||||
char *tokstr = NULL;
|
||||
char *defrealm = NULL;
|
||||
int ret;
|
||||
|
||||
if (strcmp(token_type, "Bearer") != 0)
|
||||
return KRB5_PLUGIN_NO_HANDLE; /* Not us */
|
||||
|
||||
if ((tokstr = calloc(1, token->length + 1)) == NULL)
|
||||
return ENOMEM;
|
||||
memcpy(tokstr, token->data, token->length);
|
||||
|
||||
if (realm == NULL) {
|
||||
ret = krb5_get_default_realm(context, &defrealm);
|
||||
if (ret) {
|
||||
krb5_set_error_message(context, ret, "could not determine default "
|
||||
"realm");
|
||||
free(tokstr);
|
||||
return ret;
|
||||
}
|
||||
realm = defrealm;
|
||||
}
|
||||
|
||||
ret = get_issuer_pubkeys(context, realm, &jwk_previous, &jwk_current,
|
||||
&jwk_next);
|
||||
if (ret) {
|
||||
free(defrealm);
|
||||
free(tokstr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (jwk_current.length && jwk_current.data)
|
||||
ret = cjwt_decode(tokstr, 0, &jwt, jwk_current.data,
|
||||
jwk_current.length);
|
||||
if (ret && jwk_next.length && jwk_next.data)
|
||||
ret = cjwt_decode(tokstr, 0, &jwt, jwk_next.data,
|
||||
jwk_next.length);
|
||||
if (ret && jwk_previous.length && jwk_previous.data)
|
||||
ret = cjwt_decode(tokstr, 0, &jwt, jwk_previous.data,
|
||||
jwk_previous.length);
|
||||
free(jwk_previous.data);
|
||||
free(jwk_current.data);
|
||||
free(jwk_next.data);
|
||||
jwk_previous.data = jwk_current.data = jwk_next.data = NULL;
|
||||
free(tokstr);
|
||||
tokstr = NULL;
|
||||
switch (ret) {
|
||||
case 0:
|
||||
if (jwt == NULL) {
|
||||
krb5_set_error_message(context, EINVAL, "JWT validation failed");
|
||||
free(defrealm);
|
||||
return EPERM;
|
||||
}
|
||||
if (jwt->header.alg == alg_none) {
|
||||
krb5_set_error_message(context, EINVAL, "JWT signature algorithm "
|
||||
"not supported");
|
||||
free(defrealm);
|
||||
return EPERM;
|
||||
}
|
||||
break;
|
||||
case -1:
|
||||
krb5_set_error_message(context, EINVAL, "invalid JWT format");
|
||||
free(defrealm);
|
||||
return EINVAL;
|
||||
case -2:
|
||||
krb5_set_error_message(context, EINVAL, "JWT signature validation "
|
||||
"failed (wrong issuer?)");
|
||||
free(defrealm);
|
||||
return EPERM;
|
||||
default:
|
||||
krb5_set_error_message(context, ret, "misc token validation error");
|
||||
free(defrealm);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Success; check audience */
|
||||
if ((ret = check_audience(context, realm, jwt, audiences, naudiences))) {
|
||||
cjwt_destroy(&jwt);
|
||||
free(defrealm);
|
||||
return EACCES;
|
||||
}
|
||||
|
||||
/* Success; extract principal name */
|
||||
if ((ret = get_princ(context, realm, jwt, actual_principal)) == 0) {
|
||||
token_times->authtime = jwt->iat.tv_sec;
|
||||
token_times->starttime = jwt->nbf.tv_sec;
|
||||
token_times->endtime = jwt->exp.tv_sec;
|
||||
token_times->renew_till = jwt->exp.tv_sec;
|
||||
*result = TRUE;
|
||||
}
|
||||
|
||||
cjwt_destroy(&jwt);
|
||||
free(defrealm);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL krb5_error_code
|
||||
hcjwt_init(krb5_context context, void **c)
|
||||
{
|
||||
*c = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL void
|
||||
hcjwt_fini(void *c)
|
||||
{
|
||||
}
|
||||
|
||||
static krb5plugin_token_validator_ftable plug_desc =
|
||||
{ 1, hcjwt_init, hcjwt_fini, validate };
|
||||
|
||||
static krb5plugin_token_validator_ftable *plugs[] = { &plug_desc };
|
||||
|
||||
static uintptr_t
|
||||
hcjwt_get_instance(const char *libname)
|
||||
{
|
||||
if (strcmp(libname, "krb5") == 0)
|
||||
return krb5_get_instance(libname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
krb5_plugin_load_ft kdc_token_validator_plugin_load;
|
||||
|
||||
krb5_error_code KRB5_CALLCONV
|
||||
kdc_token_validator_plugin_load(heim_pcontext context,
|
||||
krb5_get_instance_func_t *get_instance,
|
||||
size_t *num_plugins,
|
||||
krb5_plugin_common_ftable_cp **plugins)
|
||||
{
|
||||
*get_instance = hcjwt_get_instance;
|
||||
*num_plugins = sizeof(plugs) / sizeof(plugs[0]);
|
||||
*plugins = (krb5_plugin_common_ftable_cp *)plugs;
|
||||
return 0;
|
||||
}
|
||||
@@ -63,7 +63,6 @@
|
||||
|
||||
#include <microhttpd.h>
|
||||
#include "kdc_locl.h"
|
||||
#include "token_validator_plugin.h"
|
||||
#include <getarg.h>
|
||||
#include <roken.h>
|
||||
#include <krb5.h>
|
||||
|
||||
430
kdc/jwt_validator.c
Normal file
430
kdc/jwt_validator.c
Normal file
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* JWT Bearer token validator for bx509d/httpkadmind.
|
||||
*
|
||||
* Uses hx509 JOSE library for signature verification.
|
||||
*
|
||||
* Configuration:
|
||||
*
|
||||
* [bx509]
|
||||
* realms = {
|
||||
* A.REALM.NAME = {
|
||||
* # At least one of these must be set
|
||||
* jwk_current = PATH-TO-JWK-PEM-FILE
|
||||
* jwk_previous = PATH-TO-JWK-PEM-FILE
|
||||
* jwk_next = PATH-TO-JWK-PEM-FILE
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <roken.h>
|
||||
#include <krb5.h>
|
||||
#include <hx509.h>
|
||||
#include <heimbase.h>
|
||||
|
||||
#include "jwt_validator.h"
|
||||
|
||||
/*
|
||||
* Get a string value from a heim_dict_t, returning NULL if not present
|
||||
* or not a string.
|
||||
*/
|
||||
static const char *
|
||||
heim_dict_get_string(heim_dict_t dict, const char *key)
|
||||
{
|
||||
heim_string_t hkey = heim_string_create(key);
|
||||
heim_object_t val;
|
||||
const char *result = NULL;
|
||||
|
||||
if (hkey == NULL)
|
||||
return NULL;
|
||||
val = heim_dict_get_value(dict, hkey);
|
||||
heim_release(hkey);
|
||||
if (val && heim_get_tid(val) == HEIM_TID_STRING)
|
||||
result = heim_string_get_utf8((heim_string_t)val);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a number value from a heim_dict_t, returning def if not present
|
||||
* or not a number.
|
||||
*/
|
||||
static int64_t
|
||||
heim_dict_get_int(heim_dict_t dict, const char *key, int64_t def)
|
||||
{
|
||||
heim_string_t hkey = heim_string_create(key);
|
||||
heim_object_t val;
|
||||
int64_t result = def;
|
||||
|
||||
if (hkey == NULL)
|
||||
return def;
|
||||
val = heim_dict_get_value(dict, hkey);
|
||||
heim_release(hkey);
|
||||
if (val && heim_get_tid(val) == HEIM_TID_NUMBER)
|
||||
result = heim_number_get_long((heim_number_t)val);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get an object value from a heim_dict_t.
|
||||
*/
|
||||
static heim_object_t
|
||||
heim_dict_get_obj(heim_dict_t dict, const char *key)
|
||||
{
|
||||
heim_string_t hkey = heim_string_create(key);
|
||||
heim_object_t val;
|
||||
|
||||
if (hkey == NULL)
|
||||
return NULL;
|
||||
val = heim_dict_get_value(dict, hkey);
|
||||
heim_release(hkey);
|
||||
return val;
|
||||
}
|
||||
|
||||
/* JWT claims structure for KDC use */
|
||||
|
||||
typedef struct jwt_claims {
|
||||
char *sub; /* Subject */
|
||||
char *iss; /* Issuer */
|
||||
char **aud; /* Audience (array) */
|
||||
size_t aud_count;
|
||||
int64_t exp; /* Expiration time */
|
||||
int64_t nbf; /* Not before */
|
||||
int64_t iat; /* Issued at */
|
||||
char *authz_sub; /* Private claim: authz_sub */
|
||||
} jwt_claims;
|
||||
|
||||
static void
|
||||
jwt_claims_free(jwt_claims *claims)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (claims == NULL)
|
||||
return;
|
||||
free(claims->sub);
|
||||
free(claims->iss);
|
||||
for (i = 0; i < claims->aud_count; i++)
|
||||
free(claims->aud[i]);
|
||||
free(claims->aud);
|
||||
free(claims->authz_sub);
|
||||
}
|
||||
|
||||
static int
|
||||
parse_jwt_claims(const char *payload_json, size_t payload_len, jwt_claims *claims)
|
||||
{
|
||||
heim_object_t root = NULL;
|
||||
heim_object_t aud;
|
||||
const char *s;
|
||||
size_t i, k;
|
||||
|
||||
memset(claims, 0, sizeof(*claims));
|
||||
|
||||
root = heim_json_create_with_bytes(payload_json, payload_len, 10, 0, NULL);
|
||||
if (root == NULL || heim_get_tid(root) != HEIM_TID_DICT)
|
||||
goto fail;
|
||||
|
||||
/* Required claims */
|
||||
if ((s = heim_dict_get_string((heim_dict_t)root, "sub")) != NULL &&
|
||||
(claims->sub = strdup(s)) == NULL)
|
||||
goto fail;
|
||||
if ((s = heim_dict_get_string((heim_dict_t)root, "iss")) != NULL &&
|
||||
(claims->iss = strdup(s)) == NULL)
|
||||
goto fail;
|
||||
|
||||
claims->exp = heim_dict_get_int((heim_dict_t)root, "exp", 0);
|
||||
claims->nbf = heim_dict_get_int((heim_dict_t)root, "nbf", 0);
|
||||
claims->iat = heim_dict_get_int((heim_dict_t)root, "iat", 0);
|
||||
|
||||
/* Audience can be string or array of strings */
|
||||
aud = heim_dict_get_obj((heim_dict_t)root, "aud");
|
||||
if (aud) {
|
||||
if (heim_get_tid(aud) == HEIM_TID_STRING) {
|
||||
claims->aud = malloc(sizeof(char *));
|
||||
if (claims->aud) {
|
||||
claims->aud[0] = strdup(heim_string_get_utf8((heim_string_t)aud));
|
||||
if (claims->aud[0] == NULL)
|
||||
goto fail;
|
||||
claims->aud_count = 1;
|
||||
}
|
||||
} else if (heim_get_tid(aud) == HEIM_TID_ARRAY) {
|
||||
size_t count = heim_array_get_length((heim_array_t)aud);
|
||||
claims->aud = calloc(count, sizeof(char *));
|
||||
if (claims->aud) {
|
||||
for (k = i = 0; k < count; k++) {
|
||||
heim_object_t item = heim_array_get_value((heim_array_t)aud, i);
|
||||
if (item && heim_get_tid(item) == HEIM_TID_STRING) {
|
||||
claims->aud[i] = strdup(heim_string_get_utf8((heim_string_t)item));
|
||||
if (claims->aud[i] == NULL)
|
||||
goto fail;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
claims->aud_count = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Private claims */
|
||||
if ((s = heim_dict_get_string((heim_dict_t)root, "authz_sub")) != NULL &&
|
||||
(claims->authz_sub = strdup(s)) == NULL)
|
||||
goto fail;
|
||||
|
||||
heim_release(root);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
heim_release(root);
|
||||
jwt_claims_free(claims);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read a PEM file into memory.
|
||||
* Returns allocated buffer (caller must free) or NULL on error.
|
||||
*/
|
||||
static char *
|
||||
read_pem_file(const char *path, size_t *len_out)
|
||||
{
|
||||
FILE *fp;
|
||||
long len;
|
||||
char *data;
|
||||
|
||||
fp = fopen(path, "r");
|
||||
if (fp == NULL)
|
||||
return NULL;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) < 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
len = ftell(fp);
|
||||
if (len < 0 || len > 1024 * 1024) { /* Max 1MB */
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
rewind(fp);
|
||||
|
||||
data = malloc(len + 1);
|
||||
if (data == NULL) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fread(data, 1, len, fp) != (size_t)len) {
|
||||
fclose(fp);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
data[len] = '\0';
|
||||
fclose(fp);
|
||||
|
||||
if (len_out)
|
||||
*len_out = len;
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Validate a JWT Bearer token.
|
||||
*
|
||||
* Uses hx509_jws_verify() for signature verification, then performs
|
||||
* KDC-specific claims validation.
|
||||
*
|
||||
* Returns 0 on success, error code on failure.
|
||||
*/
|
||||
krb5_error_code
|
||||
validate_jwt_token(krb5_context context,
|
||||
const char *token,
|
||||
size_t token_len,
|
||||
const char * const *jwk_paths,
|
||||
size_t njwk_paths,
|
||||
const char * const *audiences,
|
||||
size_t naudiences,
|
||||
krb5_boolean *result,
|
||||
krb5_principal *actual_principal,
|
||||
krb5_times *token_times,
|
||||
const char *realm)
|
||||
{
|
||||
hx509_context hx509ctx = NULL;
|
||||
char *tokstr = NULL;
|
||||
char **pem_keys = NULL;
|
||||
void *payload = NULL;
|
||||
size_t payload_len = 0;
|
||||
jwt_claims claims;
|
||||
time_t now;
|
||||
size_t i, j, k;
|
||||
int found_aud = 0;
|
||||
krb5_error_code ret = 0;
|
||||
int hx_ret;
|
||||
|
||||
memset(&claims, 0, sizeof(claims));
|
||||
*result = FALSE;
|
||||
|
||||
if (njwk_paths == 0) {
|
||||
krb5_set_error_message(context, EINVAL, "No JWK paths provided");
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
/* Initialize hx509 context */
|
||||
hx_ret = hx509_context_init(&hx509ctx);
|
||||
if (hx_ret) {
|
||||
krb5_set_error_message(context, ENOMEM, "Could not initialize hx509 context");
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
/* Make a null-terminated copy of the token */
|
||||
tokstr = calloc(1, token_len + 1);
|
||||
if (tokstr == NULL) {
|
||||
ret = krb5_enomem(context);
|
||||
goto out;
|
||||
}
|
||||
memcpy(tokstr, token, token_len);
|
||||
|
||||
/* Read PEM key files into memory */
|
||||
pem_keys = calloc(njwk_paths, sizeof(char *));
|
||||
if (pem_keys == NULL) {
|
||||
ret = krb5_enomem(context);
|
||||
goto out;
|
||||
}
|
||||
for (k = 0; k < njwk_paths; k++) {
|
||||
if (jwk_paths[k] == NULL)
|
||||
continue;
|
||||
pem_keys[k] = read_pem_file(jwk_paths[k], NULL);
|
||||
/* It's OK if some keys fail to load - we'll try others */
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify signature using hx509.
|
||||
* This handles all the crypto: algorithm detection, key type matching,
|
||||
* ECDSA signature format conversion, EdDSA, etc.
|
||||
*/
|
||||
hx_ret = hx509_jws_verify(hx509ctx, tokstr,
|
||||
(const char * const *)pem_keys, njwk_paths,
|
||||
&payload, &payload_len);
|
||||
if (hx_ret) {
|
||||
ret = EPERM;
|
||||
krb5_set_error_message(context, ret,
|
||||
"JWT signature verification failed: %s",
|
||||
hx509_get_error_string(hx509ctx, hx_ret));
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Parse claims */
|
||||
if (parse_jwt_claims(payload, payload_len, &claims) != 0) {
|
||||
ret = EINVAL;
|
||||
krb5_set_error_message(context, ret, "Invalid JWT: could not parse claims");
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Validate exp/nbf */
|
||||
now = time(NULL);
|
||||
if (claims.exp && now >= claims.exp) {
|
||||
ret = EACCES;
|
||||
krb5_set_error_message(context, ret, "JWT token has expired");
|
||||
goto out;
|
||||
}
|
||||
if (claims.nbf && now < claims.nbf) {
|
||||
ret = EACCES;
|
||||
krb5_set_error_message(context, ret, "JWT token not yet valid");
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Validate audience (support multiple allowed audiences) */
|
||||
if (naudiences > 0) {
|
||||
for (i = 0; i < claims.aud_count && !found_aud; i++) {
|
||||
for (j = 0; j < naudiences; j++) {
|
||||
if (strcmp(claims.aud[i], audiences[j]) == 0) {
|
||||
found_aud = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found_aud) {
|
||||
ret = EACCES;
|
||||
krb5_set_error_message(context, ret, "JWT audience does not match");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extract principal */
|
||||
if (claims.authz_sub) {
|
||||
ret = krb5_parse_name(context, claims.authz_sub, actual_principal);
|
||||
} else if (claims.sub) {
|
||||
const char *at = strchr(claims.sub, '@');
|
||||
if (at) {
|
||||
ret = krb5_parse_name(context, claims.sub, actual_principal);
|
||||
} else {
|
||||
ret = krb5_parse_name_flags(context, claims.sub,
|
||||
KRB5_PRINCIPAL_PARSE_NO_REALM,
|
||||
actual_principal);
|
||||
if (ret == 0 && realm)
|
||||
ret = krb5_principal_set_realm(context, *actual_principal, realm);
|
||||
}
|
||||
} else {
|
||||
ret = EACCES;
|
||||
krb5_set_error_message(context, ret, "JWT has no subject");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
krb5_prepend_error_message(context, ret, "Could not parse JWT subject: ");
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Set times */
|
||||
token_times->authtime = claims.iat ? claims.iat : now;
|
||||
token_times->starttime = claims.nbf ? claims.nbf : claims.iat;
|
||||
token_times->endtime = claims.exp ? claims.exp : 0;
|
||||
token_times->renew_till = claims.exp ? claims.exp : 0;
|
||||
|
||||
*result = TRUE;
|
||||
|
||||
out:
|
||||
jwt_claims_free(&claims);
|
||||
if (pem_keys) {
|
||||
for (k = 0; k < njwk_paths; k++)
|
||||
free(pem_keys[k]);
|
||||
free(pem_keys);
|
||||
}
|
||||
free(tokstr);
|
||||
free(payload);
|
||||
hx509_context_free(&hx509ctx);
|
||||
return ret;
|
||||
}
|
||||
72
kdc/jwt_validator.h
Normal file
72
kdc/jwt_validator.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef HEIMDAL_KDC_JWT_VALIDATOR_H
|
||||
#define HEIMDAL_KDC_JWT_VALIDATOR_H 1
|
||||
|
||||
#include <krb5.h>
|
||||
|
||||
/*
|
||||
* Validate a JWT Bearer token using OpenSSL 3.x APIs.
|
||||
*
|
||||
* Tries multiple public keys in order (for key rotation support).
|
||||
* Signature verification succeeds if any of the provided keys validates.
|
||||
*
|
||||
* @param context Kerberos context
|
||||
* @param token The JWT token string (base64url encoded header.payload.signature)
|
||||
* @param token_len Length of the token
|
||||
* @param jwk_paths Array of paths to PEM files containing public keys
|
||||
* @param njwk_paths Number of paths (typically 1-3 for current/previous/next)
|
||||
* @param audiences Array of expected audience strings (can be NULL)
|
||||
* @param naudiences Number of audience strings
|
||||
* @param result Output: TRUE if valid
|
||||
* @param actual_principal Output: the principal from the token's subject
|
||||
* @param token_times Output: times from the token (iat, nbf, exp)
|
||||
* @param realm Default realm to use if subject has no realm
|
||||
*
|
||||
* @return 0 on success, krb5 error code on failure
|
||||
*/
|
||||
krb5_error_code
|
||||
validate_jwt_token(krb5_context context,
|
||||
const char *token,
|
||||
size_t token_len,
|
||||
const char * const *jwk_paths,
|
||||
size_t njwk_paths,
|
||||
const char * const *audiences,
|
||||
size_t naudiences,
|
||||
krb5_boolean *result,
|
||||
krb5_principal *actual_principal,
|
||||
krb5_times *token_times,
|
||||
const char *realm);
|
||||
|
||||
#endif /* HEIMDAL_KDC_JWT_VALIDATOR_H */
|
||||
@@ -1,323 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This is a plugin by which bx509d can validate Negotiate tokens.
|
||||
*
|
||||
* [kdc]
|
||||
* negotiate_token_validator = {
|
||||
* keytab = ...
|
||||
* }
|
||||
*/
|
||||
|
||||
#define _DEFAULT_SOURCE
|
||||
#define _BSD_SOURCE
|
||||
#define _GNU_SOURCE 1
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <base64.h>
|
||||
#include <roken.h>
|
||||
#include <heimbase.h>
|
||||
#include <krb5.h>
|
||||
#include <common_plugin.h>
|
||||
#include <gssapi/gssapi.h>
|
||||
#include <token_validator_plugin.h>
|
||||
|
||||
static int
|
||||
display_status(krb5_context context,
|
||||
OM_uint32 major,
|
||||
OM_uint32 minor,
|
||||
gss_cred_id_t acred,
|
||||
gss_ctx_id_t gctx,
|
||||
gss_OID mech_type)
|
||||
{
|
||||
gss_buffer_desc buf = GSS_C_EMPTY_BUFFER;
|
||||
OM_uint32 dmaj, dmin;
|
||||
OM_uint32 more = 0;
|
||||
char *gmmsg = NULL;
|
||||
char *gmsg = 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 >= INT_MAX ||
|
||||
asprintf(&s, "%s%s%.*s", gmsg ? gmsg : "", gmsg ? ": " : "",
|
||||
(int)buf.length, (char *)buf.value) == -1 ||
|
||||
s == NULL) {
|
||||
free(gmsg);
|
||||
gmsg = NULL;
|
||||
break;
|
||||
}
|
||||
gmsg = s;
|
||||
s = NULL;
|
||||
} while (!GSS_ERROR(dmaj) && more);
|
||||
if (mech_type != GSS_C_NO_OID) {
|
||||
do {
|
||||
gss_release_buffer(&dmin, &buf);
|
||||
dmaj = gss_display_status(&dmin, major, GSS_C_MECH_CODE, mech_type,
|
||||
&more, &buf);
|
||||
if (GSS_ERROR(dmaj) ||
|
||||
asprintf(&s, "%s%s%.*s", gmmsg ? gmmsg : "", gmmsg ? ": " : "",
|
||||
(int)buf.length, (char *)buf.value) == -1 ||
|
||||
s == NULL) {
|
||||
free(gmmsg);
|
||||
gmmsg = NULL;
|
||||
break;
|
||||
}
|
||||
gmmsg = s;
|
||||
s = NULL;
|
||||
} while (!GSS_ERROR(dmaj) && more);
|
||||
}
|
||||
if (gmsg == NULL)
|
||||
krb5_set_error_message(context, ENOMEM, "Error displaying GSS-API "
|
||||
"status");
|
||||
else
|
||||
krb5_set_error_message(context, EACCES, "%s%s%s%s", gmmsg,
|
||||
gmmsg ? " (" : "", gmmsg ? gmmsg : "",
|
||||
gmmsg ? ")" : "");
|
||||
if (acred && gctx)
|
||||
krb5_prepend_error_message(context, EACCES, "Failed to validate "
|
||||
"Negotiate token due to error examining "
|
||||
"GSS-API security context");
|
||||
else if (acred)
|
||||
krb5_prepend_error_message(context, EACCES, "Failed to validate "
|
||||
"Negotiate token due to error accepting "
|
||||
"GSS-API security context token");
|
||||
else
|
||||
krb5_prepend_error_message(context, EACCES, "Failed to validate "
|
||||
"Negotiate token due to error acquiring "
|
||||
"GSS-API default acceptor credential");
|
||||
return EACCES;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL krb5_error_code
|
||||
validate(void *ctx,
|
||||
krb5_context context,
|
||||
const char *realm,
|
||||
const char *token_type,
|
||||
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 = krb5_config_get_string(context, NULL, "kdc",
|
||||
"negotiate_token_validator",
|
||||
"keytab", NULL);
|
||||
OM_uint32 major, minor, ret_flags, time_rec;
|
||||
size_t i;
|
||||
char *token_decoded = NULL;
|
||||
void *token_copy = NULL;
|
||||
char *princ_str = NULL;
|
||||
int ret = 0;
|
||||
|
||||
if (strcmp(token_type, "Negotiate") != 0)
|
||||
return KRB5_PLUGIN_NO_HANDLE;
|
||||
|
||||
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)
|
||||
return display_status(context, major, minor, acred, gctx, mech_type);
|
||||
|
||||
mechs.count = 1;
|
||||
major = gss_set_neg_mechs(&minor, acred, &mechs);
|
||||
if (major != GSS_S_COMPLETE)
|
||||
return display_status(context, major, minor, acred, gctx, mech_type);
|
||||
} /* else we'll use the default credential */
|
||||
|
||||
if ((token_decoded = malloc(token->length)) == NULL ||
|
||||
(token_copy = calloc(1, token->length + 1)) == NULL)
|
||||
goto enomem;
|
||||
|
||||
memcpy(token_copy, token->data, token->length);
|
||||
if ((ret = rk_base64_decode(token_copy, token_decoded)) <= 0) {
|
||||
krb5_set_error_message(context, EACCES, "Negotiate token malformed");
|
||||
ret = EACCES;
|
||||
goto out;
|
||||
}
|
||||
|
||||
input_token.value = token_decoded;
|
||||
input_token.length = ret;
|
||||
major = gss_accept_sec_context(&minor, &gctx, acred, &input_token, NULL,
|
||||
&iname, &mech_type, &output_token,
|
||||
&ret_flags, &time_rec, NULL);
|
||||
|
||||
if (mech_type == GSS_C_NO_OID ||
|
||||
!gss_oid_equal(mech_type, GSS_KRB5_MECHANISM)) {
|
||||
krb5_set_error_message(context, ret = EACCES, "Negotiate token used "
|
||||
"non-Kerberos mechanism");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (major != GSS_S_COMPLETE) {
|
||||
ret = display_status(context, major, minor, acred, gctx, mech_type);
|
||||
if (ret == 0)
|
||||
ret = EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
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 = display_status(context, major, minor, acred, gctx, mech_type);
|
||||
if (ret == 0)
|
||||
ret = EINVAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
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) {
|
||||
/* This handles the case where naudiences == 0 as an error */
|
||||
krb5_set_error_message(context, EACCES, "Negotiate token used "
|
||||
"wrong HTTP service host acceptor name");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if ((princ_str = calloc(1, idisplay_name.length + 1)) == NULL)
|
||||
goto enomem;
|
||||
memcpy(princ_str, idisplay_name.value, idisplay_name.length);
|
||||
if ((ret = krb5_parse_name(context, princ_str, actual_principal)))
|
||||
goto out;
|
||||
|
||||
/* XXX Need name attributes to get authtime/starttime/renew_till */
|
||||
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;
|
||||
goto out;
|
||||
|
||||
enomem:
|
||||
ret = krb5_enomem(context);
|
||||
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;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL krb5_error_code
|
||||
negotiate_init(krb5_context context, void **c)
|
||||
{
|
||||
*c = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static KRB5_LIB_CALL void
|
||||
negotiate_fini(void *c)
|
||||
{
|
||||
}
|
||||
|
||||
static krb5plugin_token_validator_ftable plug_desc =
|
||||
{ 1, negotiate_init, negotiate_fini, validate };
|
||||
|
||||
static krb5plugin_token_validator_ftable *plugs[] = { &plug_desc };
|
||||
|
||||
static uintptr_t
|
||||
negotiate_get_instance(const char *libname)
|
||||
{
|
||||
if (strcmp(libname, "krb5") == 0)
|
||||
return krb5_get_instance(libname);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
krb5_plugin_load_ft kdc_token_validator_plugin_load;
|
||||
|
||||
krb5_error_code KRB5_CALLCONV
|
||||
kdc_token_validator_plugin_load(heim_pcontext context,
|
||||
krb5_get_instance_func_t *get_instance,
|
||||
size_t *num_plugins,
|
||||
krb5_plugin_common_ftable_cp **plugins)
|
||||
{
|
||||
*get_instance = negotiate_get_instance;
|
||||
*num_plugins = sizeof(plugs) / sizeof(plugs[0]);
|
||||
*plugins = (krb5_plugin_common_ftable_cp *)plugs;
|
||||
return 0;
|
||||
}
|
||||
345
kdc/test_jwt_validator.c
Normal file
345
kdc/test_jwt_validator.c
Normal file
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright (c) 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Test program for JWT validator using RFC 7515 and RFC 8037 test vectors.
|
||||
*/
|
||||
|
||||
#include <config.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <krb5.h>
|
||||
#include "jwt_validator.h"
|
||||
|
||||
/*
|
||||
* RFC 7515 Appendix A.2 - RS256 test vector
|
||||
*
|
||||
* The token has claims: {"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
|
||||
* Note: exp is in the past (2011), so we test signature only, not expiration.
|
||||
*/
|
||||
static const char *rfc7515_rs256_token =
|
||||
"eyJhbGciOiJSUzI1NiJ9"
|
||||
"."
|
||||
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
|
||||
"."
|
||||
"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw";
|
||||
|
||||
/* RSA public key from RFC 7515 A.2 in PEM format */
|
||||
static const char *rfc7515_rs256_pubkey_pem =
|
||||
"-----BEGIN PUBLIC KEY-----\n"
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAofgWCuLjybRlzo0tZWJj\n"
|
||||
"NiuSfb4p4fAkd/wWJcyQoTbji9k0l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEz\n"
|
||||
"P1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo\n"
|
||||
"9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTB\n"
|
||||
"EMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxX\n"
|
||||
"FvUK+DWNmoudF8NAco9/h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXp\n"
|
||||
"oQIDAQAB\n"
|
||||
"-----END PUBLIC KEY-----\n";
|
||||
|
||||
/*
|
||||
* RFC 7515 Appendix A.3 - ES256 test vector
|
||||
*
|
||||
* Same claims as RS256.
|
||||
*/
|
||||
static const char *rfc7515_es256_token =
|
||||
"eyJhbGciOiJFUzI1NiJ9"
|
||||
"."
|
||||
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
|
||||
"."
|
||||
"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q";
|
||||
|
||||
/* EC P-256 public key from RFC 7515 A.3 in PEM format */
|
||||
static const char *rfc7515_es256_pubkey_pem =
|
||||
"-----BEGIN PUBLIC KEY-----\n"
|
||||
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf83OJ3D2xF1Bg8vub9tLe1gHMzV7\n"
|
||||
"6e8Tus9uPHvRVEXH8UTNG72bfocs3+257rn0s2ldbqkLJK2KRiMohYjlrQ==\n"
|
||||
"-----END PUBLIC KEY-----\n";
|
||||
|
||||
/*
|
||||
* RFC 8037 Appendix A.4 - EdDSA (Ed25519) test vector
|
||||
*
|
||||
* Payload is "Example of Ed25519 signing" (not JSON claims)
|
||||
*/
|
||||
static const char *rfc8037_eddsa_token =
|
||||
"eyJhbGciOiJFZERTQSJ9"
|
||||
"."
|
||||
"RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"
|
||||
"."
|
||||
"hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg";
|
||||
|
||||
/* Ed25519 public key from RFC 8037 A.4 in PEM format */
|
||||
static const char *rfc8037_ed25519_pubkey_pem =
|
||||
"-----BEGIN PUBLIC KEY-----\n"
|
||||
"MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n"
|
||||
"-----END PUBLIC KEY-----\n";
|
||||
|
||||
static int
|
||||
write_temp_key(const char *pem, char *path, size_t pathlen)
|
||||
{
|
||||
int fd;
|
||||
ssize_t len = strlen(pem);
|
||||
|
||||
snprintf(path, pathlen, "/tmp/jwt_test_key_XXXXXX");
|
||||
fd = mkstemp(path);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, pem, len) != len) {
|
||||
close(fd);
|
||||
unlink(path);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test signature verification only (not claims validation).
|
||||
* We pass no audiences and ignore expiration since RFC test vectors
|
||||
* have expired exp claims.
|
||||
*/
|
||||
static int
|
||||
test_signature_only(krb5_context context,
|
||||
const char *name,
|
||||
const char *token,
|
||||
const char *pubkey_pem)
|
||||
{
|
||||
char keypath[256];
|
||||
const char *paths[1];
|
||||
krb5_boolean result = FALSE;
|
||||
krb5_principal princ = NULL;
|
||||
krb5_times times;
|
||||
krb5_error_code ret;
|
||||
|
||||
printf("Testing %s signature verification... ", name);
|
||||
fflush(stdout);
|
||||
|
||||
if (write_temp_key(pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
||||
printf("FAILED (could not write temp key)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
paths[0] = keypath;
|
||||
|
||||
/*
|
||||
* Note: validate_jwt_token validates exp/nbf claims, but the RFC
|
||||
* test vectors have expired tokens. We're primarily testing signature
|
||||
* verification here, so we accept EACCES for expired tokens as long
|
||||
* as we get past signature verification.
|
||||
*/
|
||||
ret = validate_jwt_token(context,
|
||||
token, strlen(token),
|
||||
paths, 1,
|
||||
NULL, 0, /* no audience check */
|
||||
&result, &princ, ×,
|
||||
"TEST.REALM");
|
||||
|
||||
unlink(keypath);
|
||||
|
||||
/*
|
||||
* For RFC test vectors, we expect either:
|
||||
* - Success (result == TRUE)
|
||||
* - EACCES with "expired" in the error message (signature was valid)
|
||||
* - EACCES with "no subject" (signature was valid, but payload isn't JWT claims)
|
||||
* - EINVAL with "could not parse claims" (signature was valid, but payload isn't JSON)
|
||||
*/
|
||||
if (ret == 0 && result) {
|
||||
printf("OK\n");
|
||||
krb5_free_principal(context, princ);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ret == EACCES || ret == EINVAL) {
|
||||
const char *msg = krb5_get_error_message(context, ret);
|
||||
if (strstr(msg, "expired") || strstr(msg, "no subject") ||
|
||||
strstr(msg, "not valid JSON") || strstr(msg, "could not parse claims")) {
|
||||
printf("OK (signature valid, %s)\n",
|
||||
strstr(msg, "expired") ? "token expired" : "non-JWT payload");
|
||||
krb5_free_error_message(context, msg);
|
||||
krb5_free_principal(context, princ);
|
||||
return 0;
|
||||
}
|
||||
printf("FAILED: %s\n", msg);
|
||||
krb5_free_error_message(context, msg);
|
||||
} else if (ret) {
|
||||
const char *msg = krb5_get_error_message(context, ret);
|
||||
printf("FAILED: %s\n", msg);
|
||||
krb5_free_error_message(context, msg);
|
||||
} else {
|
||||
printf("FAILED: result=%d\n", result);
|
||||
}
|
||||
|
||||
krb5_free_principal(context, princ);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test with a tampered token (should fail signature verification).
|
||||
*/
|
||||
static int
|
||||
test_tampered_token(krb5_context context)
|
||||
{
|
||||
char keypath[256];
|
||||
const char *paths[1];
|
||||
krb5_boolean result = FALSE;
|
||||
krb5_principal princ = NULL;
|
||||
krb5_times times;
|
||||
krb5_error_code ret;
|
||||
char *tampered;
|
||||
|
||||
printf("Testing tampered token rejection... ");
|
||||
fflush(stdout);
|
||||
|
||||
/* Copy and tamper with the token (change one character in payload) */
|
||||
tampered = strdup(rfc7515_rs256_token);
|
||||
if (!tampered) {
|
||||
printf("FAILED (out of memory)\n");
|
||||
return 1;
|
||||
}
|
||||
/* Find the payload and change a character */
|
||||
tampered[50] = (tampered[50] == 'a') ? 'b' : 'a';
|
||||
|
||||
if (write_temp_key(rfc7515_rs256_pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
||||
printf("FAILED (could not write temp key)\n");
|
||||
free(tampered);
|
||||
return 1;
|
||||
}
|
||||
|
||||
paths[0] = keypath;
|
||||
|
||||
ret = validate_jwt_token(context,
|
||||
tampered, strlen(tampered),
|
||||
paths, 1,
|
||||
NULL, 0,
|
||||
&result, &princ, ×,
|
||||
"TEST.REALM");
|
||||
|
||||
unlink(keypath);
|
||||
free(tampered);
|
||||
|
||||
if (ret == EPERM && !result) {
|
||||
printf("OK (correctly rejected)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("FAILED: tampered token was accepted!\n");
|
||||
krb5_free_principal(context, princ);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test wrong key rejection.
|
||||
*/
|
||||
static int
|
||||
test_wrong_key(krb5_context context)
|
||||
{
|
||||
char keypath[256];
|
||||
const char *paths[1];
|
||||
krb5_boolean result = FALSE;
|
||||
krb5_principal princ = NULL;
|
||||
krb5_times times;
|
||||
krb5_error_code ret;
|
||||
|
||||
printf("Testing wrong key rejection... ");
|
||||
fflush(stdout);
|
||||
|
||||
/* Use ES256 key to verify RS256 token - should fail */
|
||||
if (write_temp_key(rfc7515_es256_pubkey_pem, keypath, sizeof(keypath)) < 0) {
|
||||
printf("FAILED (could not write temp key)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
paths[0] = keypath;
|
||||
|
||||
ret = validate_jwt_token(context,
|
||||
rfc7515_rs256_token, strlen(rfc7515_rs256_token),
|
||||
paths, 1,
|
||||
NULL, 0,
|
||||
&result, &princ, ×,
|
||||
"TEST.REALM");
|
||||
|
||||
unlink(keypath);
|
||||
|
||||
if (ret == EPERM && !result) {
|
||||
printf("OK (correctly rejected)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("FAILED: wrong key type was accepted!\n");
|
||||
krb5_free_principal(context, princ);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
krb5_context context;
|
||||
krb5_error_code ret;
|
||||
int failures = 0;
|
||||
|
||||
ret = krb5_init_context(&context);
|
||||
if (ret) {
|
||||
fprintf(stderr, "krb5_init_context failed: %d\n", ret);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("JWT Validator Test Suite\n");
|
||||
printf("========================\n\n");
|
||||
|
||||
printf("RFC 7515 Test Vectors:\n");
|
||||
failures += test_signature_only(context, "RS256", rfc7515_rs256_token,
|
||||
rfc7515_rs256_pubkey_pem);
|
||||
failures += test_signature_only(context, "ES256", rfc7515_es256_token,
|
||||
rfc7515_es256_pubkey_pem);
|
||||
|
||||
printf("\nRFC 8037 Test Vectors:\n");
|
||||
failures += test_signature_only(context, "EdDSA (Ed25519)", rfc8037_eddsa_token,
|
||||
rfc8037_ed25519_pubkey_pem);
|
||||
|
||||
printf("\nNegative Tests:\n");
|
||||
failures += test_tampered_token(context);
|
||||
failures += test_wrong_key(context);
|
||||
|
||||
printf("\n");
|
||||
if (failures == 0) {
|
||||
printf("All tests passed!\n");
|
||||
} else {
|
||||
printf("%d test(s) failed!\n", failures);
|
||||
}
|
||||
|
||||
krb5_free_context(context);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Kungliga Tekniska Högskolan
|
||||
* Copyright (c) 2019-2025 Kungliga Tekniska Högskolan
|
||||
* (Royal Institute of Technology, Stockholm, Sweden).
|
||||
* All rights reserved.
|
||||
*
|
||||
@@ -31,52 +31,354 @@
|
||||
* 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 "token_validator_plugin.h"
|
||||
#include <base64.h>
|
||||
#include <gssapi/gssapi.h>
|
||||
#include "jwt_validator.h"
|
||||
|
||||
struct plctx {
|
||||
const char *realm;
|
||||
const char *token_kind;
|
||||
krb5_data *token;
|
||||
const char * const *audiences;
|
||||
size_t naudiences;
|
||||
krb5_boolean result;
|
||||
krb5_principal actual_principal;
|
||||
krb5_times token_times;
|
||||
};
|
||||
|
||||
static krb5_error_code KRB5_LIB_CALL
|
||||
plcallback(krb5_context context, const void *plug, void *plugctx, void *userctx)
|
||||
/*
|
||||
* Get configuration value from [bx509] realms section.
|
||||
*/
|
||||
static const char *
|
||||
get_kv(krb5_context context, const char *realm, const char *k, const char *k2)
|
||||
{
|
||||
const krb5plugin_token_validator_ftable *validator = plug;
|
||||
krb5_error_code ret;
|
||||
struct plctx *plctx = userctx;
|
||||
return krb5_config_get_string(context, NULL, "bx509", "realms", realm,
|
||||
k, k2, NULL);
|
||||
}
|
||||
|
||||
ret = validator->validate(plugctx, context, plctx->realm,
|
||||
plctx->token_kind, plctx->token,
|
||||
plctx->audiences, plctx->naudiences,
|
||||
&plctx->result, &plctx->actual_principal,
|
||||
&plctx->token_times);
|
||||
if (ret) {
|
||||
krb5_free_principal(context, plctx->actual_principal);
|
||||
plctx->actual_principal = 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;
|
||||
}
|
||||
|
||||
static const char *plugin_deps[] = { "krb5", NULL };
|
||||
/*
|
||||
* 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;
|
||||
|
||||
static struct heim_plugin_data token_validator_data = {
|
||||
"kdc",
|
||||
KDC_PLUGIN_BEARER,
|
||||
1,
|
||||
plugin_deps,
|
||||
krb5_get_instance
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Invoke a plugin to validate a JWT/SAML/OIDC token and partially-evaluate
|
||||
* access control.
|
||||
* 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,
|
||||
@@ -89,34 +391,45 @@ kdc_validate_token(krb5_context context,
|
||||
krb5_times *token_times)
|
||||
{
|
||||
krb5_error_code ret;
|
||||
struct plctx ctx;
|
||||
krb5_boolean result = FALSE;
|
||||
krb5_times times;
|
||||
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.realm = realm;
|
||||
ctx.token_kind = token_kind;
|
||||
ctx.token = token;
|
||||
ctx.audiences = audiences;
|
||||
ctx.naudiences = naudiences;
|
||||
ctx.result = FALSE;
|
||||
ctx.actual_principal = NULL;
|
||||
memset(×, 0, sizeof(times));
|
||||
if (actual_principal)
|
||||
*actual_principal = NULL;
|
||||
|
||||
krb5_clear_error_message(context);
|
||||
ret = _krb5_plugin_run_f(context, &token_validator_data, 0, &ctx,
|
||||
plcallback);
|
||||
if (ret == 0 && ctx.result && actual_principal) {
|
||||
*actual_principal = ctx.actual_principal;
|
||||
ctx.actual_principal = NULL;
|
||||
|
||||
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 = ctx.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;
|
||||
}
|
||||
}
|
||||
|
||||
krb5_free_principal(context, ctx.actual_principal);
|
||||
if (ret)
|
||||
krb5_prepend_error_message(context, ret, "bearer token validation "
|
||||
"failed: ");
|
||||
else if (!ctx.result)
|
||||
krb5_set_error_message(context, ret = EACCES,
|
||||
"bearer token validation failed");
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ testfailed="echo test failed; cat messages.log; exit 1"
|
||||
|
||||
. ${env_setup}
|
||||
|
||||
srcdir="${top_srcdir}/tests/kdc"
|
||||
|
||||
# If there is no useful db support compiled in, disable test
|
||||
${have_db} || exit 77
|
||||
|
||||
@@ -113,6 +115,13 @@ rm -rf authz_dir
|
||||
|
||||
mkdir -p authz_dir
|
||||
|
||||
# Generate JWT signing keys upfront (needed before config file is read)
|
||||
echo "Generating JWT signing keys"
|
||||
openssl genpkey -algorithm RSA -out ${objdir}/jwt_sign.pem -pkeyopt rsa_keygen_bits:2048 2>/dev/null ||
|
||||
{ echo "Failed to generate JWT signing key"; exit 2; }
|
||||
openssl pkey -in ${objdir}/jwt_sign.pem -pubout -out ${objdir}/jwt_sign_pub.pem 2>/dev/null ||
|
||||
{ echo "Failed to extract JWT public key"; exit 2; }
|
||||
|
||||
> messages.log
|
||||
|
||||
kdcpid=
|
||||
@@ -279,6 +288,43 @@ token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server)
|
||||
$test_token_validator -a datan.test.h5l.se Negotiate "$token" ||
|
||||
{ echo "Negotiate token validator failed to validate valid token"; exit 2; }
|
||||
|
||||
echo "Testing JWT validator with RFC test vectors"
|
||||
${objdir}/../../kdc/test_jwt_validator ||
|
||||
{ echo "JWT validator RFC test vectors failed"; exit 2; }
|
||||
|
||||
echo "Check Bearer/JWT token validator"
|
||||
# JWT signing key was already generated at start of test
|
||||
|
||||
# Generate a valid JWT using hxtool
|
||||
jwt_token=$($hxtool jwt-sign -k ${objdir}/jwt_sign.pem -a RS256 \
|
||||
-s "foo@${R}" -i "test-issuer" -A "datan.test.h5l.se" -l 3600 | tr -d '\n')
|
||||
|
||||
echo "Testing Bearer token validator with valid JWT"
|
||||
$test_token_validator -a datan.test.h5l.se Bearer "$jwt_token" ||
|
||||
{ echo "Bearer token validator failed to validate valid JWT"; exit 2; }
|
||||
|
||||
echo "Testing Bearer token validator with wrong audience"
|
||||
$test_token_validator -a wrong.audience.example Bearer "$jwt_token" &&
|
||||
{ echo "Bearer token validator accepted JWT with wrong audience"; exit 2; }
|
||||
|
||||
echo "Testing Bearer token validator with tampered JWT"
|
||||
tampered_jwt=$(echo "$jwt_token" | sed 's/eyJ/eXJ/')
|
||||
$test_token_validator -a datan.test.h5l.se Bearer "$tampered_jwt" &&
|
||||
{ echo "Bearer token validator accepted tampered JWT"; exit 2; }
|
||||
|
||||
echo "Testing Bearer token validator with expired JWT"
|
||||
# Use negative lifetime to create an already-expired token
|
||||
expired_jwt=$($hxtool jwt-sign -k ${objdir}/jwt_sign.pem -a RS256 \
|
||||
-s "foo@${R}" -i "test-issuer" -A "datan.test.h5l.se" -l -3600 | tr -d '\n')
|
||||
$test_token_validator -a datan.test.h5l.se Bearer "$expired_jwt" &&
|
||||
{ echo "Bearer token validator accepted expired JWT"; exit 2; }
|
||||
|
||||
# Note: ES256 and EdDSA signature verification is tested by the RFC test vectors
|
||||
# in test_jwt_validator. The Bearer token tests use only RS256 because the
|
||||
# configuration only supports a single key file.
|
||||
|
||||
echo "JWT/Bearer token validator tests passed"
|
||||
|
||||
|
||||
echo "Starting CSR authorizer IPC service"
|
||||
$test_csr_authorizer --server --daemon ||
|
||||
|
||||
@@ -79,8 +79,8 @@ kadminr="${kadmin} -r $R -a $(uname -n)"
|
||||
kadmin="${kadmin} -l -A -r $R"
|
||||
kadmind2="${kadmind} --keytab=${keytab} --detach -p $admport2 --read-only"
|
||||
kadmind="${kadmind} -A --keytab=${keytab} --detach -p $admport"
|
||||
httpkadmind2="${httpkadmind} -A --reverse-proxied -T Negotiate -p $restport2"
|
||||
httpkadmind="${httpkadmind} -A --reverse-proxied -T Negotiate -p $restport1"
|
||||
httpkadmind2="${httpkadmind} -A --reverse-proxied -T Negotiate -T Bearer -p $restport2"
|
||||
httpkadmind="${httpkadmind} -A --reverse-proxied -T Negotiate -T Bearer -p $restport1"
|
||||
|
||||
kinit2="${kinit} -c $cache2 ${afs_no_afslog}"
|
||||
kinit="${kinit} -c $cache ${afs_no_afslog}"
|
||||
@@ -113,6 +113,13 @@ mkdir -p authz_dir
|
||||
|
||||
> messages.log
|
||||
|
||||
# Generate JWT signing keys for Bearer token tests
|
||||
echo "Generating JWT signing keys"
|
||||
openssl genpkey -algorithm RSA -out ${objdir}/jwt_sign.pem -pkeyopt rsa_keygen_bits:2048 2>/dev/null ||
|
||||
{ echo "Failed to generate JWT signing key"; exit 2; }
|
||||
openssl pkey -in ${objdir}/jwt_sign.pem -pubout -out ${objdir}/jwt_sign_pub.pem 2>/dev/null ||
|
||||
{ echo "Failed to extract JWT public key"; exit 2; }
|
||||
|
||||
# We'll avoid using a KDC for now. For testing /httpkadmind we only need keys
|
||||
# for Negotiate tokens, and we'll use ktutil and kimpersonate to make it
|
||||
# possible to create and accept those without a KDC.
|
||||
@@ -149,6 +156,19 @@ HTTP() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
# HTTP_JWT jwt-token curl-opts
|
||||
# Use Bearer token authentication instead of Negotiate
|
||||
HTTP_JWT() {
|
||||
jwt_token="$1"
|
||||
shift
|
||||
curl -g --resolve ${server}:${restport1}:127.0.0.1 \
|
||||
--resolve ${server}:${restport2}:127.0.0.1 \
|
||||
-H "Authorization: Bearer ${jwt_token}" \
|
||||
$verbose \
|
||||
-D response-headers \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# get_config QPARAMS curl-opts
|
||||
get_config() {
|
||||
url="http://${server}:${restport}/get-config?$1"
|
||||
@@ -352,6 +372,61 @@ ${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.rest ||
|
||||
cmp extracted_keytab.kadmin extracted_keytab.rest ||
|
||||
{ echo "Keytabs for $p don't match!"; exit 1; }
|
||||
|
||||
# JWT Bearer token tests
|
||||
echo "Testing JWT Bearer token authentication"
|
||||
hn=foo.ns.${domain}
|
||||
p=HTTP/$hn
|
||||
echo "Fetching keytab for virtual principal $p using JWT Bearer token"
|
||||
rm -f extracted_keytab*
|
||||
grant san_dnsname $hn foo@${R}
|
||||
|
||||
# Get expected keytab via kadmin for comparison
|
||||
${kadmin} ext_keytab -k extracted_keytab $p ||
|
||||
{ echo "Failed to get a keytab for $p with kadmin"; exit 1; }
|
||||
${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.kadmin ||
|
||||
{ echo "Failed to list keytab for $p"; exit 1; }
|
||||
|
||||
# Generate a JWT token for foo@TEST.H5L.SE
|
||||
jwt_token=$($hxtool jwt-sign -k ${objdir}/jwt_sign.pem -a RS256 \
|
||||
-s "foo@${R}" -i "test-issuer" -A "${server}" -l 3600 | tr -d '\n') ||
|
||||
{ echo "Failed to generate JWT token"; exit 1; }
|
||||
|
||||
# Use JWT Bearer authentication to fetch keytab
|
||||
rm -f extracted_keytab
|
||||
url="http://${server}:${restport}/get-keys?dNSName=${hn}"
|
||||
HTTP_JWT "$jwt_token" $verbose -sf -o "${objdir}/extracted_keytab" "$url" ||
|
||||
{ echo "Failed to get a keytab for $p with JWT Bearer token"; exit 1; }
|
||||
${ktutil} -k "${objdir}/extracted_keytab" list --keys > extracted_keytab.jwt ||
|
||||
{ echo "Failed to list keytab for $p"; exit 1; }
|
||||
cmp extracted_keytab.kadmin extracted_keytab.jwt ||
|
||||
{ echo "Keytabs for $p don't match (JWT vs kadmin)!"; exit 1; }
|
||||
|
||||
echo "Testing JWT Bearer token with invalid signature fails"
|
||||
# Create a token with wrong key (we'll just mangle the signature)
|
||||
bad_jwt_token="${jwt_token}x"
|
||||
url="http://${server}:${restport}/get-keys?dNSName=${hn}"
|
||||
HTTP_JWT "$bad_jwt_token" $verbose -sf -o "${objdir}/extracted_keytab" "$url" &&
|
||||
{ echo "Got keytab with invalid JWT token!"; exit 1; }
|
||||
|
||||
echo "Testing JWT Bearer token with wrong subject fails authorization"
|
||||
revoke
|
||||
jwt_token_other=$($hxtool jwt-sign -k ${objdir}/jwt_sign.pem -a RS256 \
|
||||
-s "other@${R}" -i "test-issuer" -A "${server}" -l 3600 | tr -d '\n') ||
|
||||
{ echo "Failed to generate JWT token"; exit 1; }
|
||||
# foo@R is not authorized, only granted to other@R, but we're using foo@R's JWT
|
||||
grant san_dnsname $hn other@${R}
|
||||
url="http://${server}:${restport}/get-keys?dNSName=${hn}"
|
||||
HTTP_JWT "$jwt_token" $verbose -sf -o "${objdir}/extracted_keytab" "$url" &&
|
||||
{ echo "Got keytab with unauthorized JWT subject!"; exit 1; }
|
||||
|
||||
# But other@R's JWT should work
|
||||
HTTP_JWT "$jwt_token_other" $verbose -sf -o "${objdir}/extracted_keytab" "$url" ||
|
||||
{ echo "Failed to get keytab with authorized JWT subject"; exit 1; }
|
||||
|
||||
# Restore authorization for foo@R for subsequent tests
|
||||
revoke
|
||||
grant san_dnsname $hn foo@${R}
|
||||
|
||||
hn1=foo.ns.${domain}
|
||||
hn2=foobar.ns.${domain}
|
||||
hn3=xyz.${domain}
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
[bx509]
|
||||
realms = {
|
||||
TEST.H5L.SE = {
|
||||
# JWT token validation keys
|
||||
jwk_current = @objdir@/jwt_sign_pub.pem
|
||||
# Default (no cert exts requested)
|
||||
user = {
|
||||
# Use an issuer for user certs:
|
||||
|
||||
@@ -96,3 +96,10 @@
|
||||
|
||||
[domain_realm]
|
||||
. = TEST.H5L.SE
|
||||
|
||||
[bx509]
|
||||
realms = {
|
||||
TEST.H5L.SE = {
|
||||
jwk_current = @objdir@/jwt_sign_pub.pem
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user