Add bx509d
This commit is contained in:
330
kdc/cjwt_token_validator.c
Normal file
330
kdc/cjwt_token_validator.c
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* 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 <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/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 == next->length &&
|
||||
memcmp(current->data, next->data, next->length) == 0) {
|
||||
free(next->data);
|
||||
next->data = 0;
|
||||
next->length = 0;
|
||||
}
|
||||
if (current->length == previous->length &&
|
||||
memcmp(current->data, previous->data, previous->length) == 0) {
|
||||
free(previous->data);
|
||||
previous->data = 0;
|
||||
previous->length = 0;
|
||||
}
|
||||
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 ((ret = cjwt_decode(tokstr, 0, &jwt, jwk_current.data,
|
||||
jwk_current.length)) == -2 &&
|
||||
(ret = cjwt_decode(tokstr, 0, &jwt, jwk_next.data,
|
||||
jwk_next.length)) == -2)
|
||||
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->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(krb5_context 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;
|
||||
}
|
Reference in New Issue
Block a user