X-Git-Url: http://git.samba.org/?p=samba.git;a=blobdiff_plain;f=source4%2Fheimdal%2Fkdc%2Fcjwt_token_validator.c;fp=source4%2Fheimdal%2Fkdc%2Fcjwt_token_validator.c;h=68fe01594f8e7ba7072bfb6677af50b1681e737d;hp=0000000000000000000000000000000000000000;hb=40b65c840e03bd5eb7f3b02fe80144650c63c005;hpb=d2a3016a9c59f93f89cf4bb86d40938d56400453 diff --git a/source4/heimdal/kdc/cjwt_token_validator.c b/source4/heimdal/kdc/cjwt_token_validator.c new file mode 100644 index 00000000000..68fe01594f8 --- /dev/null +++ b/source4/heimdal/kdc/cjwt_token_validator.c @@ -0,0 +1,338 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_CJSON +#include +#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; + } + + 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->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; +}