summaryrefslogtreecommitdiffstats
path: root/third_party/heimdal/kdc/cjwt_token_validator.c
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/heimdal/kdc/cjwt_token_validator.c')
-rw-r--r--third_party/heimdal/kdc/cjwt_token_validator.c343
1 files changed, 343 insertions, 0 deletions
diff --git a/third_party/heimdal/kdc/cjwt_token_validator.c b/third_party/heimdal/kdc/cjwt_token_validator.c
new file mode 100644
index 0000000..93742e5
--- /dev/null
+++ b/third_party/heimdal/kdc/cjwt_token_validator.c
@@ -0,0 +1,343 @@
+/*
+ * 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, &current->data, &current->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;
+}