Pass a threadsafe ctx into fr_connection_pool create callback
[freeradius.git] / src / modules / rlm_krb5 / rlm_krb5.c
1 /*
2  *   This program is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or
5  *   (at your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the Free Software
14  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16
17 /**
18  * $Id$
19  * @file rlm_krb5.c
20  * @brief Authenticate users, retrieving their TGT from a Kerberos V5 TDC.
21  *
22  * @copyright 2000,2006,2012-2013  The FreeRADIUS server project
23  * @copyright 2013  Arran Cudbard-Bell <a.cudbardb@freeradius.org>
24  * @copyright 2000  Nathan Neulinger <nneul@umr.edu>
25  * @copyright 2000  Alan DeKok <aland@ox.org>
26  */
27 RCSID("$Id$")
28
29 #include <freeradius-devel/radiusd.h>
30 #include <freeradius-devel/modules.h>
31 #include <freeradius-devel/rad_assert.h>
32 #include "krb5.h"
33
34 static const CONF_PARSER module_config[] = {
35         { "keytab", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_krb5_t, keytabname), NULL },
36         { "service_principal", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_krb5_t, service_princ), NULL },
37         { NULL, -1, 0, NULL, NULL }
38 };
39
40 static int mod_detach(void *instance)
41 {
42         rlm_krb5_t *inst = instance;
43
44 #ifndef HEIMDAL_KRB5
45         talloc_free(inst->vic_options);
46
47         if (inst->gic_options) {
48                 krb5_get_init_creds_opt_free(inst->context, inst->gic_options);
49         }
50
51         if (inst->server) {
52                 krb5_free_principal(inst->context, inst->server);
53         }
54 #endif
55
56         /* Don't free hostname, it's just a pointer into service_princ */
57         talloc_free(inst->service);
58
59         if (inst->context) {
60                 krb5_free_context(inst->context);
61         }
62 #ifdef KRB5_IS_THREAD_SAFE
63         fr_connection_pool_delete(inst->pool);
64 #endif
65
66         return 0;
67 }
68
69 static int mod_instantiate(CONF_SECTION *conf, void *instance)
70 {
71         rlm_krb5_t *inst = instance;
72         krb5_error_code ret;
73 #ifndef HEIMDAL_KRB5
74         krb5_keytab keytab;
75         char keytab_name[200];
76         char *princ_name;
77 #endif
78
79 #ifdef HEIMDAL_KRB5
80         DEBUG("Using Heimdal Kerberos library");
81 #else
82         DEBUG("Using MIT Kerberos library");
83 #endif
84
85         if (!krb5_is_thread_safe()) {
86 /*
87  *      rlm_krb5 was built as threadsafe
88  */
89 #ifdef KRB5_IS_THREAD_SAFE
90                 ERROR("Build time libkrb5 was threadsafe, but run time library claims not to be");
91                 ERROR("Modify runtime linker path (LD_LIBRARY_PATH on most systems), to prefer threadsafe libkrb5");
92                 return -1;
93 /*
94  *      rlm_krb5 was not built as threadsafe
95  */
96 #else
97                 WARN("libkrb5 is not threadsafe, recompile it with thread support enabled ("
98 #  ifdef HEIMDAL_KRB5
99                        "--enable-pthread-support"
100 #  else
101                        "--disable-thread-support=no"
102 #  endif
103                        ")");
104                 WARN("rlm_krb5 will run in single threaded mode, performance may be degraded");
105         } else {
106                 WARN("Build time libkrb5 was not threadsafe, but run time library claims to be");
107                 WARN("Reconfigure and recompile rlm_krb5 to enable thread support");
108 #endif
109         }
110
111         inst->xlat_name = cf_section_name2(conf);
112         if (!inst->xlat_name) {
113                 inst->xlat_name = cf_section_name1(conf);
114         }
115
116         ret = krb5_init_context(&inst->context);
117         if (ret) {
118                 ERROR("rlm_krb5 (%s): context initialisation failed: %s", inst->xlat_name,
119                       rlm_krb5_error(NULL, ret));
120
121                 return -1;
122         }
123
124         /*
125          *      Split service principal into service and host components
126          *      they're needed to build the server principal in MIT,
127          *      and to set the validation service in Heimdal.
128          */
129         if (inst->service_princ) {
130                 size_t len;
131                 /* Service principal appears to contain a host component */
132                 inst->hostname = strchr(inst->service_princ, '/');
133                 if (inst->hostname) {
134                         len = (inst->hostname - inst->service_princ);
135                         inst->hostname++;
136                 } else {
137                         len = strlen(inst->service_princ);
138                 }
139
140                 if (len) {
141                         inst->service = talloc_array(inst, char, (len + 1));
142                         strlcpy(inst->service, inst->service_princ, len + 1);
143                 }
144         }
145
146 #ifdef HEIMDAL_KRB5
147         if (inst->hostname) {
148                 DEBUG("rlm_krb5 (%s): Ignoring hostname component of service principal \"%s\", not "
149                       "needed/supported by Heimdal", inst->xlat_name, inst->hostname);
150         }
151 #else
152
153         /*
154          *      Convert the service principal string to a krb5 principal.
155          */
156         ret = krb5_sname_to_principal(inst->context, inst->hostname, inst->service, KRB5_NT_SRV_HST, &(inst->server));
157         if (ret) {
158                 ERROR("rlm_krb5 (%s): Failed parsing service principal: %s", inst->xlat_name,
159                       rlm_krb5_error(inst->context, ret));
160
161                 return -1;
162         }
163
164         ret = krb5_unparse_name(inst->context, inst->server, &princ_name);
165         if (ret) {
166                 /* Uh? */
167                 ERROR("rlm_krb5 (%s): Failed constructing service principal string: %s", inst->xlat_name,
168                       rlm_krb5_error(inst->context, ret));
169
170                 return -1;
171         }
172
173         /*
174          *      Not necessarily the same as the config item
175          */
176         DEBUG("rlm_krb5 (%s): Using service principal \"%s\"", inst->xlat_name, princ_name);
177
178         krb5_free_unparsed_name(inst->context, princ_name);
179
180         /*
181          *      Setup options for getting credentials and verifying them
182          */
183
184         /* For some reason the 'init' version of this function is deprecated */
185         ret = krb5_get_init_creds_opt_alloc(inst->context, &(inst->gic_options));
186         if (ret) {
187                 ERROR("rlm_krb5 (%s): Couldn't allocated inital credential options: %s", inst->xlat_name,
188                       rlm_krb5_error(inst->context, ret));
189
190                 return -1;
191         }
192
193         /*
194          *      Perform basic checks on the keytab
195          */
196         ret = inst->keytabname ?
197                 krb5_kt_resolve(inst->context, inst->keytabname, &keytab) :
198                 krb5_kt_default(inst->context, &keytab);
199         if (ret) {
200                 ERROR("rlm_krb5 (%s): Resolving keytab failed: %s", inst->xlat_name,
201                       rlm_krb5_error(inst->context, ret));
202
203                 return -1;
204         }
205
206         ret = krb5_kt_get_name(inst->context, keytab, keytab_name, sizeof(keytab_name));
207         krb5_kt_close(inst->context, keytab);
208         if (ret) {
209                 ERROR("rlm_krb5 (%s): Can't retrieve keytab name: %s", inst->xlat_name,
210                       rlm_krb5_error(inst->context, ret));
211
212                 return -1;
213         }
214
215         DEBUG("rlm_krb5 (%s): Using keytab \"%s\"", inst->xlat_name, keytab_name);
216
217         MEM(inst->vic_options = talloc_zero(inst, krb5_verify_init_creds_opt));
218         krb5_verify_init_creds_opt_init(inst->vic_options);
219 #endif
220
221 #ifdef KRB5_IS_THREAD_SAFE
222         /*
223          *      Initialize the socket pool.
224          */
225         inst->pool = fr_connection_pool_init(conf, inst, mod_conn_create, NULL, NULL, NULL);
226         if (!inst->pool) {
227                 return -1;
228         }
229 #else
230         inst->conn = mod_conn_create(inst, inst);
231         if (!inst->conn) {
232                 return -1;
233         }
234 #endif
235         return 0;
236 }
237
238 /** Common function for transforming a User-Name string into a principal.
239  *
240  * @param[out] client Where to write the client principal.
241  * @param[in] request Current request.
242  * @param[in] context Kerberos context.
243  */
244 static rlm_rcode_t krb5_parse_user(krb5_principal *client, REQUEST *request, krb5_context context)
245 {
246         krb5_error_code ret;
247         char *princ_name;
248
249         /*
250          *      We can only authenticate user requests which HAVE
251          *      a User-Name attribute.
252          */
253         if (!request->username) {
254                 REDEBUG("Attribute \"User-Name\" is required for authentication");
255
256                 return RLM_MODULE_INVALID;
257         }
258
259         /*
260          *      We can only authenticate user requests which HAVE
261          *      a User-Password attribute.
262          */
263         if (!request->password) {
264                 REDEBUG("Attribute \"User-Password\" is required for authentication");
265
266                 return RLM_MODULE_INVALID;
267         }
268
269         /*
270          *      Ensure that we're being passed a plain-text password,
271          *      and not anything else.
272          */
273         if (request->password->da->attr != PW_USER_PASSWORD) {
274                 REDEBUG("Attribute \"User-Password\" is required for authentication.  Cannot use \"%s\".",
275                         request->password->da->name);
276
277                 return RLM_MODULE_INVALID;
278         }
279
280         ret = krb5_parse_name(context, request->username->vp_strvalue, client);
281         if (ret) {
282                 REDEBUG("Failed parsing username as principal: %s", rlm_krb5_error(context, ret));
283
284                 return RLM_MODULE_FAIL;
285         }
286
287         krb5_unparse_name(context, *client, &princ_name);
288         RDEBUG("Using client principal \"%s\"", princ_name);
289 #ifdef HEIMDAL_KRB5
290         free(princ_name);
291 #else
292         krb5_free_unparsed_name(context, princ_name);
293 #endif
294         return RLM_MODULE_OK;
295 }
296
297 /** Log error message and return appropriate rcode
298  *
299  * Translate kerberos error codes into return codes.
300  * @param request Current request.
301  * @param ret code from kerberos.
302  * @param conn used in the last operation.
303  */
304 static rlm_rcode_t krb5_process_error(REQUEST *request, rlm_krb5_handle_t *conn, int ret)
305 {
306         rad_assert(ret != 0);
307         rad_assert(conn);       /* Silences warnings */
308
309         switch (ret) {
310         case KRB5_LIBOS_BADPWDMATCH:
311         case KRB5KRB_AP_ERR_BAD_INTEGRITY:
312                 REDEBUG("Provided password was incorrect (%i): %s", ret, rlm_krb5_error(conn->context, ret));
313                 return RLM_MODULE_REJECT;
314
315         case KRB5KDC_ERR_KEY_EXP:
316         case KRB5KDC_ERR_CLIENT_REVOKED:
317         case KRB5KDC_ERR_SERVICE_REVOKED:
318                 REDEBUG("Account has been locked out (%i): %s", ret, rlm_krb5_error(conn->context, ret));
319                 return RLM_MODULE_USERLOCK;
320
321         case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
322                 RDEBUG("User not found (%i): %s", ret, rlm_krb5_error(conn->context, ret));
323                 return RLM_MODULE_NOTFOUND;
324
325         default:
326                 REDEBUG("Error verifying credentials (%i): %s", ret, rlm_krb5_error(conn->context, ret));
327                 return RLM_MODULE_FAIL;
328         }
329 }
330
331 #ifdef HEIMDAL_KRB5
332
333 /*
334  *      Validate user/pass (Heimdal)
335  */
336 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
337 {
338         rlm_krb5_t *inst = instance;
339         rlm_rcode_t rcode;
340         krb5_error_code ret;
341
342         rlm_krb5_handle_t *conn;
343
344         krb5_principal client;
345
346 #ifdef KRB5_IS_THREAD_SAFE
347         conn = fr_connection_get(inst->pool);
348         if (!conn) return RLM_MODULE_FAIL;
349 #else
350         conn = inst->conn;
351 #endif
352
353         /*
354          *      Zero out local storage
355          */
356         memset(&client, 0, sizeof(client));
357
358         rcode = krb5_parse_user(&client, request, conn->context);
359         if (rcode != RLM_MODULE_OK) goto cleanup;
360
361         /*
362          *      Verify the user, using the options we set in instantiate
363          */
364         ret = krb5_verify_user_opt(conn->context, client, request->password->vp_strvalue, &conn->options);
365         if (ret) {
366                 rcode = krb5_process_error(request, conn, ret);
367                 goto cleanup;
368         }
369
370         /*
371          *      krb5_verify_user_opt adds the credentials to the ccache
372          *      we specified with krb5_verify_opt_set_ccache.
373          *
374          *      To make sure we don't accumulate thousands of sets of
375          *      credentials, remove them again here.
376          *
377          * @todo This should definitely be optional, which means writing code for the MIT
378          *       variant as well.
379          */
380         {
381                 krb5_cc_cursor cursor;
382                 krb5_creds cred;
383
384                 krb5_cc_start_seq_get(conn->context, conn->ccache, &cursor);
385                 for ((ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred));
386                      ret == 0;
387                      (ret = krb5_cc_next_cred(conn->context, conn->ccache, &cursor, &cred))) {
388                      krb5_cc_remove_cred(conn->context, conn->ccache, 0, &cred);
389                 }
390                 krb5_cc_end_seq_get(conn->context, conn->ccache, &cursor);
391         }
392
393 cleanup:
394         if (client) {
395                 krb5_free_principal(conn->context, client);
396         }
397
398 #ifdef KRB5_IS_THREAD_SAFE
399         fr_connection_release(inst->pool, conn);
400 #endif
401         return rcode;
402 }
403
404 #else  /* HEIMDAL_KRB5 */
405
406 /*
407  *  Validate userid/passwd (MIT)
408  */
409 static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request)
410 {
411         rlm_krb5_t *inst = instance;
412         rlm_rcode_t rcode;
413         krb5_error_code ret;
414
415         rlm_krb5_handle_t *conn;
416
417         krb5_principal client;
418         krb5_creds init_creds;
419         char *password;         /* compiler warnings */
420
421         rad_assert(inst->context);
422
423 #ifdef KRB5_IS_THREAD_SAFE
424         conn = fr_connection_get(inst->pool);
425         if (!conn) return RLM_MODULE_FAIL;
426 #else
427         conn = inst->conn;
428 #endif
429
430         /*
431          *      Zero out local storage
432          */
433         memset(&client, 0, sizeof(client));
434         memset(&init_creds, 0, sizeof(init_creds));
435
436         /*
437          *      Check we have all the required VPs, and convert the username
438          *      into a principal.
439          */
440         rcode = krb5_parse_user(&client, request, conn->context);
441         if (rcode != RLM_MODULE_OK) goto cleanup;
442
443         /*
444          *      Retrieve the TGT from the TGS/KDC and check we can decrypt it.
445          */
446         memcpy(&password, &request->password->vp_strvalue, sizeof(password));
447         RDEBUG("Retrieving and decrypting TGT");
448         ret = krb5_get_init_creds_password(conn->context, &init_creds, client, password,
449                                            NULL, NULL, 0, NULL, inst->gic_options);
450         if (ret) {
451                 rcode = krb5_process_error(request, conn, ret);
452                 goto cleanup;
453         }
454
455         RDEBUG("Attempting to authenticate against service principal");
456         ret = krb5_verify_init_creds(conn->context, &init_creds, inst->server, conn->keytab, NULL, inst->vic_options);
457         if (ret) {
458                 rcode = krb5_process_error(request, conn, ret);
459         }
460
461 cleanup:
462         if (client) {
463                 krb5_free_principal(conn->context, client);
464         }
465         krb5_free_cred_contents(conn->context, &init_creds);
466
467 #ifdef KRB5_IS_THREAD_SAFE
468         fr_connection_release(inst->pool, conn);
469 #endif
470         return rcode;
471 }
472
473 #endif /* MIT_KRB5 */
474
475 module_t rlm_krb5 = {
476         RLM_MODULE_INIT,
477         "krb5",
478         RLM_TYPE_HUP_SAFE
479 #ifdef KRB5_IS_THREAD_SAFE
480         | RLM_TYPE_THREAD_SAFE
481 #endif
482         ,
483         sizeof(rlm_krb5_t),
484         module_config,
485         mod_instantiate,                /* instantiation */
486         mod_detach,                     /* detach */
487         {
488                 mod_authenticate,       /* authenticate */
489                 NULL,                   /* authorize */
490                 NULL,                   /* pre-accounting */
491                 NULL,                   /* accounting */
492                 NULL,                   /* checksimul */
493                 NULL,                   /* pre-proxy */
494                 NULL,                   /* post-proxy */
495                 NULL                    /* post-auth */
496         },
497 };