heimdal: Honour KRB5_CTX_F_DNS_CANONICALIZE_HOSTNAME in parse_name_canon_rules()
[metze/heimdal/wip.git] / kdc / cjwt_token_validator.c
1 /*
2  * Copyright (c) 2019 Kungliga Tekniska Högskolan
3  * (Royal Institute of Technology, Stockholm, Sweden).
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  *
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in the
15  *    documentation and/or other materials provided with the distribution.
16  *
17  * 3. Neither the name of the Institute nor the names of its contributors
18  *    may be used to endorse or promote products derived from this software
19  *    without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24  * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31  * SUCH DAMAGE.
32  */
33
34 /*
35  * This is a plugin by which bx509d can validate JWT Bearer tokens using the
36  * cjwt library.
37  *
38  * Configuration:
39  *
40  *  [kdc]
41  *      realm = {
42  *          A.REALM.NAME = {
43  *              cjwt_jqk = PATH-TO-JWK-PEM-FILE
44  *          }
45  *      }
46  *
47  * where AUDIENCE-FOR-KDC is the value of the "audience" (i.e., the target) of
48  * the token.
49  */
50
51 #include <config.h>
52 #include <errno.h>
53 #include <stdio.h>
54 #include <string.h>
55 #include <string.h>
56 #include <heimbase.h>
57 #include <krb5.h>
58 #include <common_plugin.h>
59 #include <hdb.h>
60 #include <roken.h>
61 #include <token_validator_plugin.h>
62 #include <cjwt/cjwt.h>
63 #ifdef HAVE_CJSON
64 #include <cJSON.h>
65 #endif
66
67 static const char *
68 get_kv(krb5_context context, const char *realm, const char *k, const char *k2)
69 {
70     return krb5_config_get_string(context, NULL, "bx509", "realms", realm,
71                                   k, k2, NULL);
72 }
73
74 static krb5_error_code
75 get_issuer_pubkeys(krb5_context context,
76                    const char *realm,
77                    krb5_data *previous,
78                    krb5_data *current,
79                    krb5_data *next)
80 {
81     krb5_error_code save_ret = 0;
82     krb5_error_code ret;
83     const char *v;
84     size_t nkeys = 0;
85
86     previous->data = current->data = next->data = 0;
87     previous->length = current->length = next->length = 0;
88
89     if ((v = get_kv(context, realm, "cjwt_jwk_next", NULL)) &&
90         (++nkeys) &&
91         (ret = rk_undumpdata(v, &next->data, &next->length)))
92         save_ret = ret;
93     if ((v = get_kv(context, realm, "cjwt_jwk_previous", NULL)) &&
94         (++nkeys) &&
95         (ret = rk_undumpdata(v, &previous->data, &previous->length)) &&
96         save_ret == 0)
97         save_ret = ret;
98     if ((v = get_kv(context, realm, "cjwt_jwk_current", NULL)) &&
99         (++nkeys) &&
100         (ret = rk_undumpdata(v, &current->data, &current->length)) &&
101         save_ret == 0)
102         save_ret = ret;
103     if (nkeys == 0)
104         krb5_set_error_message(context, EINVAL, "jwk issuer key not specified in "
105                                "[bx509]->realm->%s->cjwt_jwk_{previous,current,next}",
106                                realm);
107     if (!previous->length && !current->length && !next->length)
108         krb5_set_error_message(context, save_ret,
109                                "Could not read jwk issuer public key files");
110     if (current->length == next->length &&
111         memcmp(current->data, next->data, next->length) == 0) {
112         free(next->data);
113         next->data = 0;
114         next->length = 0;
115     }
116     if (current->length == previous->length &&
117         memcmp(current->data, previous->data, previous->length) == 0) {
118         free(previous->data);
119         previous->data = 0;
120         previous->length = 0;
121     }
122
123     if (previous->data == NULL && current->data == NULL && next->data == NULL)
124         return krb5_set_error_message(context, ENOENT, "No JWKs found"),
125                ENOENT;
126     return 0;
127 }
128
129 static krb5_error_code
130 check_audience(krb5_context context,
131                const char *realm,
132                cjwt_t *jwt,
133                const char * const *audiences,
134                size_t naudiences)
135 {
136     size_t i, k;
137
138     if (!jwt->aud) {
139         krb5_set_error_message(context, EACCES, "JWT bearer token has no "
140                                "audience");
141         return EACCES;
142     }
143     for (i = 0; i < jwt->aud->count; i++)
144         for (k = 0; k < naudiences; k++)
145             if (strcasecmp(audiences[k], jwt->aud->names[i]) == 0)
146                 return 0;
147     krb5_set_error_message(context, EACCES, "JWT bearer token's audience "
148                            "does not match any expected audience");
149     return EACCES;
150 }
151
152 static krb5_error_code
153 get_princ(krb5_context context,
154           const char *realm,
155           cjwt_t *jwt,
156           krb5_principal *actual_principal)
157 {
158     krb5_error_code ret;
159     const char *force_realm = NULL;
160     const char *domain;
161
162 #ifdef HAVE_CJSON
163     if (jwt->private_claims) {
164         cJSON *jval;
165
166         if ((jval = cJSON_GetObjectItem(jwt->private_claims, "authz_sub")))
167             return krb5_parse_name(context, jval->valuestring, actual_principal);
168     }
169 #endif
170
171     if (jwt->sub == NULL) {
172         krb5_set_error_message(context, EACCES, "JWT token lacks 'sub' "
173                                "(subject name)!");
174         return EACCES;
175     }
176     if ((domain = strchr(jwt->sub, '@'))) {
177         force_realm = get_kv(context, realm, "cjwt_force_realm", ++domain);
178         ret = krb5_parse_name(context, jwt->sub, actual_principal);
179     } else {
180         ret = krb5_parse_name_flags(context, jwt->sub,
181                                     KRB5_PRINCIPAL_PARSE_NO_REALM,
182                                     actual_principal);
183     }
184     if (ret)
185         krb5_set_error_message(context, ret, "JWT token 'sub' not a valid "
186                                "principal name: %s", jwt->sub);
187     else if (force_realm)
188         ret = krb5_principal_set_realm(context, *actual_principal, realm);
189     else if (domain == NULL)
190         ret = krb5_principal_set_realm(context, *actual_principal, realm);
191     /* else leave the domain as the realm */
192     return ret;
193 }
194
195 static KRB5_LIB_CALL krb5_error_code
196 validate(void *ctx,
197          krb5_context context,
198          const char *realm,
199          const char *token_type,
200          krb5_data *token,
201          const char * const *audiences,
202          size_t naudiences,
203          krb5_boolean *result,
204          krb5_principal *actual_principal,
205          krb5_times *token_times)
206 {
207     heim_octet_string jwk_previous;
208     heim_octet_string jwk_current;
209     heim_octet_string jwk_next;
210     cjwt_t *jwt = NULL;
211     char *tokstr = NULL;
212     char *defrealm = NULL;
213     int ret;
214
215     if (strcmp(token_type, "Bearer") != 0)
216         return KRB5_PLUGIN_NO_HANDLE; /* Not us */
217
218     if ((tokstr = calloc(1, token->length + 1)) == NULL)
219         return ENOMEM;
220     memcpy(tokstr, token->data, token->length);
221
222     if (realm == NULL) {
223         ret = krb5_get_default_realm(context, &defrealm);
224         if (ret) {
225             krb5_set_error_message(context, ret, "could not determine default "
226                                    "realm");
227             free(tokstr);
228             return ret;
229         }
230         realm = defrealm;
231     }
232
233     ret = get_issuer_pubkeys(context, realm, &jwk_previous, &jwk_current,
234                              &jwk_next);
235     if (ret) {
236         free(defrealm);
237         free(tokstr);
238         return ret;
239     }
240
241     if (jwk_current.length && jwk_current.data)
242         ret = cjwt_decode(tokstr, 0, &jwt, jwk_current.data,
243                           jwk_current.length);
244     if (ret && jwk_next.length && jwk_next.data)
245         ret = cjwt_decode(tokstr, 0, &jwt, jwk_next.data,
246                             jwk_next.length);
247     if (ret && jwk_previous.length && jwk_previous.data)
248         ret = cjwt_decode(tokstr, 0, &jwt, jwk_previous.data,
249                           jwk_previous.length);
250     free(jwk_previous.data);
251     free(jwk_current.data);
252     free(jwk_next.data);
253     jwk_previous.data = jwk_current.data = jwk_next.data = NULL;
254     free(tokstr);
255     tokstr = NULL;
256     switch (ret) {
257     case 0:
258         if (jwt->header.alg == alg_none) {
259             krb5_set_error_message(context, EINVAL, "JWT signature algorithm "
260                                    "not supported");
261             free(defrealm);
262             return EPERM;
263         }
264         break;
265     case -1:
266         krb5_set_error_message(context, EINVAL, "invalid JWT format");
267         free(defrealm);
268         return EINVAL;
269     case -2:
270         krb5_set_error_message(context, EINVAL, "JWT signature validation "
271                                "failed (wrong issuer?)");
272         free(defrealm);
273         return EPERM;
274     default:
275         krb5_set_error_message(context, ret, "misc token validation error");
276         free(defrealm);
277         return ret;
278     }
279
280     /* Success; check audience */
281     if ((ret = check_audience(context, realm, jwt, audiences, naudiences))) {
282         cjwt_destroy(&jwt);
283         free(defrealm);
284         return EACCES;
285     }
286
287     /* Success; extract principal name */
288     if ((ret = get_princ(context, realm, jwt, actual_principal)) == 0) {
289         token_times->authtime   = jwt->iat.tv_sec;
290         token_times->starttime  = jwt->nbf.tv_sec;
291         token_times->endtime    = jwt->exp.tv_sec;
292         token_times->renew_till = jwt->exp.tv_sec;
293         *result = TRUE;
294     }
295
296     cjwt_destroy(&jwt);
297     free(defrealm);
298     return ret;
299 }
300
301 static KRB5_LIB_CALL krb5_error_code
302 hcjwt_init(krb5_context context, void **c)
303 {
304     *c = NULL;
305     return 0;
306 }
307
308 static KRB5_LIB_CALL void
309 hcjwt_fini(void *c)
310 {
311 }
312
313 static krb5plugin_token_validator_ftable plug_desc =
314     { 1, hcjwt_init, hcjwt_fini, validate };
315
316 static krb5plugin_token_validator_ftable *plugs[] = { &plug_desc };
317
318 static uintptr_t
319 hcjwt_get_instance(const char *libname)
320 {
321     if (strcmp(libname, "krb5") == 0)
322         return krb5_get_instance(libname);
323     return 0;
324 }
325
326 krb5_plugin_load_ft kdc_token_validator_plugin_load;
327
328 krb5_error_code KRB5_CALLCONV
329 kdc_token_validator_plugin_load(heim_pcontext context,
330                                 krb5_get_instance_func_t *get_instance,
331                                 size_t *num_plugins,
332                                 krb5_plugin_common_ftable_cp **plugins)
333 {
334     *get_instance = hcjwt_get_instance;
335     *num_plugins = sizeof(plugs) / sizeof(plugs[0]);
336     *plugins = (krb5_plugin_common_ftable_cp *)plugs;
337     return 0;
338 }